Skip to main content

nodejs/
builtins.rs

1//! Builtin op handlers (compiler-emitted `CallBuiltin` ids) plus the JS standard
2//! library (`console`, `Math`, `JSON`, `Object`, array/string methods) reachable
3//! from the host. Handlers pop their arguments off the VM operand stack and
4//! return the result value, which the VM pushes back.
5
6use crate::host::{self, ops, with_host, FuncVal, JsObj};
7use fusevm::{NumOp, Value, VM};
8use indexmap::IndexMap;
9
10/// Register every node-js builtin id on a VM.
11pub fn install(vm: &mut VM) {
12    vm.register_builtin(ops::GETLOCAL, b_getlocal);
13    vm.register_builtin(ops::SETLOCAL, b_setlocal);
14    vm.register_builtin(ops::DECLARE, b_declare);
15    vm.register_builtin(ops::DELNAME, b_delname);
16    vm.register_builtin(ops::GETATTR, b_getattr);
17    vm.register_builtin(ops::SETATTR, b_setattr);
18    vm.register_builtin(ops::GETITEM, b_getitem);
19    vm.register_builtin(ops::SETITEM, b_setitem);
20    vm.register_builtin(ops::DELITEM, b_delitem);
21    vm.register_builtin(ops::MKSTR, b_mkstr);
22    vm.register_builtin(ops::MKARR, b_mkarr);
23    vm.register_builtin(ops::MKOBJ, b_mkobj);
24    vm.register_builtin(ops::CALL, b_call);
25    vm.register_builtin(ops::CALL_METHOD, b_call_method);
26    vm.register_builtin(ops::CALL_VALUE, b_call_value);
27    vm.register_builtin(ops::NEW, b_new);
28    vm.register_builtin(ops::TRUTHY, b_truthy);
29    vm.register_builtin(ops::TOSTR, b_tostr);
30    vm.register_builtin(ops::MKFUNC, b_mkfunc);
31    vm.register_builtin(ops::GETITER, b_getiter);
32    vm.register_builtin(ops::FORITER, b_foriter);
33    vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
34    vm.register_builtin(ops::CONTAINS, b_contains);
35    vm.register_builtin(ops::SIG_RETURN, b_sig_return);
36    vm.register_builtin(ops::BINOP, b_binop);
37    vm.register_builtin(ops::UNARY, b_unary);
38    vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
39    vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
40    vm.register_builtin(ops::TYPEOF, b_typeof);
41    vm.register_builtin(ops::LOAD_NULL, b_load_null);
42    vm.register_builtin(ops::THROW, b_throw);
43    vm.register_builtin(ops::TRY, b_try);
44    vm.register_builtin(ops::NULLISH, b_nullish);
45    vm.register_builtin(ops::UNPACK, b_unpack);
46    vm.register_builtin(ops::BUILD_ARGS, b_build_args);
47    vm.register_builtin(ops::THIS, b_this);
48    vm.register_builtin(ops::INSTANCEOF, b_instanceof);
49    vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
50    vm.register_builtin(ops::APPLY, b_apply);
51    vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
52    vm.register_builtin(ops::OBJ_REST, b_obj_rest);
53    vm.register_builtin(ops::DIV, b_div);
54    vm.register_builtin(ops::MKCLASS, b_mkclass);
55    vm.register_builtin(ops::DEF_MEMBER, b_def_member);
56    vm.register_builtin(ops::DEF_FIELD, b_def_field);
57    vm.register_builtin(ops::SUPER_CALL, b_super_call);
58    vm.register_builtin(ops::SUPER_GET, b_super_get);
59    vm.register_builtin(ops::YIELD, b_yield);
60    vm.register_builtin(ops::PROPKEY, b_propkey);
61    vm.register_builtin(ops::NEW_TARGET, b_new_target);
62    vm.register_builtin(ops::AWAIT, b_await);
63    vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
64    vm.register_builtin(ops::DBG_LINE, b_dbg_line);
65    vm.register_builtin(ops::MKBIGINT, b_mkbigint);
66    vm.register_builtin(ops::MKREGEX, b_mkregex);
67    vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
68    vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
69    vm.register_builtin(ops::ASYNC_STEP, b_async_step);
70    vm.register_builtin(ops::NUM_STEP, b_num_step);
71    vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
72    vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
73}
74
75/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
76/// runs its pending `finally`; a user iterator object gets its `.return()` called
77/// if present; a plain materialized iterator just drops. Returns `undefined`.
78fn b_iter_close(vm: &mut VM, _: u8) -> Value {
79    let it = vm.pop();
80    if with_host(|h| h.is_generator_val(&it)) {
81        // Ignore the close outcome; a `finally` may print/yield but the loop is
82        // done. Preserve any error it raises (uncaught finally throw propagates).
83        if let Err(e) = host::gen_return(&it, Value::Undef) {
84            return abort(vm, e);
85        }
86        return Value::Undef;
87    }
88    // A user iterator object with a `.return()` method (iterator protocol close).
89    if matches!(with_host(|h| h.get(&it).cloned()), Some(JsObj::Object(_))) {
90        if let Some(f) = with_host(|h| host::lookup_chain(h, &it, "return")) {
91            if with_host(|h| host::is_callable(h, &f)) {
92                if let Err(e) = host::invoke(&f, Vec::new(), Some(it.clone())) {
93                    return abort(vm, e);
94                }
95            }
96        }
97    }
98    Value::Undef
99}
100
101/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
102/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
103/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
104/// Number nor throws the mix error.
105fn b_num_step(vm: &mut VM, _: u8) -> Value {
106    let old = vm.pop();
107    let tag = match vm.pop() {
108        Value::Int(n) => n,
109        Value::Float(f) => f as i64,
110        _ => 1,
111    };
112    if with_host(|h| h.is_bigint_val(&old)) {
113        let b = with_host(|h| h.as_bigint(&old)).unwrap();
114        let old_n = with_host(|h| h.new_bigint(b.clone()));
115        let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
116        vm.push(old_n);
117        new
118    } else {
119        let n = with_host(|h| h.to_number(&old));
120        vm.push(Value::Float(n));
121        Value::Float(n + tag as f64)
122    }
123}
124
125/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
126/// `{value, done}` record (see `host::async_step`).
127fn b_async_step(vm: &mut VM, _: u8) -> Value {
128    let iter = vm.pop();
129    let r = host::async_step(&iter);
130    finish(vm, r)
131}
132
133/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
134/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
135fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
136    let digits = sval(&vm.pop());
137    match digits.parse::<num_bigint::BigInt>() {
138        Ok(b) => with_host(|h| h.new_bigint(b)),
139        Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
140    }
141}
142
143/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
144/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
145/// Builds the `strings` array (carrying its `.raw` array) and calls
146/// `tag(strings, ...values)`.
147fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
148    let mut all = pop_n(vm, argc as usize);
149    let int_of = |v: &Value| match v {
150        Value::Int(n) => *n as usize,
151        Value::Float(f) => *f as usize,
152        _ => 0,
153    };
154    let tag = all.remove(0);
155    let n = int_of(&all.remove(0));
156    let mcount = int_of(&all.remove(0));
157    let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
158    let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
159    let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
160    // strings = cooked array; strings.raw = raw array (frozen in JS; nothing here
161    // mutates it).
162    let strings = with_host(|h| h.new_array(cooked));
163    let raw_arr = with_host(|h| h.new_array(raw));
164    with_host(|h| h.set_fn_prop(&strings, "raw", raw_arr));
165    let mut call_args = vec![strings];
166    call_args.extend(values);
167    let r = host::invoke(&tag, call_args, None);
168    finish(vm, r)
169}
170
171/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
172/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
173/// iterator (each yielded value is awaited). Returns the iterator object/handle.
174fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
175    let src = vm.pop();
176    let r = host::get_async_iterator(&src);
177    finish(vm, r)
178}
179
180/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
181/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
182/// express (backreference/lookaround) throws a `SyntaxError` here.
183fn b_mkregex(vm: &mut VM, _: u8) -> Value {
184    let flags = sval(&vm.pop());
185    let pattern = sval(&vm.pop());
186    match crate::regexp::build_regexp(&pattern, &flags) {
187        Ok(v) => v,
188        Err(e) => abort(vm, e),
189    }
190}
191
192/// DAP per-statement marker (`node --dap` only; the compiler emits this before
193/// each statement under `debug`). Pops the source line pushed by the preceding
194/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
195/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
196/// session is active.
197fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
198    let line = match vm.pop() {
199        Value::Int(n) => n as u32,
200        _ => 0,
201    };
202    crate::dap::on_debug_line(line);
203    Value::Undef
204}
205
206/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
207/// or `member::SET`). Keeps the object on the stack.
208fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
209    let func = vm.pop();
210    let kind = match vm.pop() {
211        Value::Int(n) => n,
212        _ => 0,
213    };
214    let name = sval(&vm.pop());
215    let obj = vm.pop();
216    with_host(|h| {
217        if kind == host::member::SET {
218            h.set_accessor(&obj, &name, None, Some(func));
219        } else {
220            h.set_accessor(&obj, &name, Some(func), None);
221        }
222    });
223    obj
224}
225
226fn b_await(vm: &mut VM, _: u8) -> Value {
227    let v = vm.pop();
228    match host::await_value(v) {
229        Ok(r) => r,
230        Err(e) => abort(vm, e),
231    }
232}
233
234// ── classes / super / generators / property keys (compiler-emitted ops) ──────
235
236fn b_mkclass(vm: &mut VM, _: u8) -> Value {
237    let ctor = vm.pop();
238    let parent = vm.pop();
239    let name = sval(&vm.pop());
240    host::build_class(&name, parent, ctor)
241}
242
243fn b_def_member(vm: &mut VM, _: u8) -> Value {
244    let func = vm.pop();
245    let is_static = matches!(vm.pop(), Value::Bool(true));
246    let kind = match vm.pop() {
247        Value::Int(n) => n,
248        _ => 0,
249    };
250    let name = sval(&vm.pop());
251    let class_val = vm.pop();
252    host::define_member(&class_val, &name, kind, is_static, func);
253    class_val
254}
255
256fn b_def_field(vm: &mut VM, _: u8) -> Value {
257    let thunk = vm.pop();
258    let name = sval(&vm.pop());
259    let class_val = vm.pop();
260    host::define_field(&class_val, &name, thunk);
261    class_val
262}
263
264/// `super(...args)` in a derived constructor: run the parent constructor on the
265/// current `this`, then this class's field initializers.
266fn b_super_call(vm: &mut VM, argc: u8) -> Value {
267    let args = pop_n(vm, argc as usize);
268    let this = with_host(|h| h.current_this());
269    let this = match this {
270        Some(t) => t,
271        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
272    };
273    // The class whose constructor is running = the running method's home class.
274    let (parent, fields) = with_host(|h| h.super_context());
275    let (parent, fields) = match parent {
276        Some(p) => (p, fields),
277        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
278    };
279    let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
280    let r = host::super_construct(&parent, args, &this, &nt);
281    if let Err(e) = r {
282        return abort(vm, e);
283    }
284    // Run this (derived) class's own instance-field initializers after super.
285    for (name, thunk) in fields {
286        match host::invoke(&thunk, Vec::new(), Some(this.clone())) {
287            Ok(val) => with_host(|h| {
288                if let Some(JsObj::Object(props)) = h.get_mut(&this) {
289                    props.insert(name, val);
290                }
291            }),
292            Err(e) => return abort(vm, e),
293        }
294    }
295    Value::Undef
296}
297
298/// `super.name` — a method from the parent's prototype, or a getter's result.
299fn b_super_get(vm: &mut VM, _: u8) -> Value {
300    let name = sval(&vm.pop());
301    match with_host(|h| h.super_resolve(&name)) {
302        host::SuperRef::Data(v) => v,
303        host::SuperRef::Getter(getter) => {
304            let this = with_host(|h| h.current_this());
305            match host::invoke(&getter, Vec::new(), this) {
306                Ok(v) => v,
307                Err(e) => abort(vm, e),
308            }
309        }
310    }
311}
312
313fn b_yield(vm: &mut VM, _: u8) -> Value {
314    let v = vm.pop();
315    match host::gen_yield(v) {
316        Ok(sent) => {
317            // A `.return()`/`.throw()` injected on resume sets a pending Return
318            // signal (or error); halt the chunk so the body unwinds through any
319            // enclosing `try/finally`, exactly like a source `return`/`throw`.
320            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
321                vm.ip = vm.chunk.ops.len();
322            }
323            sent
324        }
325        Err(e) => abort(vm, e),
326    }
327}
328
329fn b_propkey(vm: &mut VM, _: u8) -> Value {
330    let v = vm.pop();
331    let k = with_host(|h| h.property_key(&v));
332    with_host(|h| h.new_str(k))
333}
334
335fn b_new_target(_vm: &mut VM, _: u8) -> Value {
336    with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
337}
338
339/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
340/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
341/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
342/// `0/0 === NaN`, so `/` is lowered here instead. Non-number operands are coerced
343/// via `ToNumber`, exactly as the numeric hook's `arith(Div)` path does.
344fn b_div(vm: &mut VM, _: u8) -> Value {
345    let b = vm.pop();
346    let a = vm.pop();
347    let r = with_host(|h| h.arith(NumOp::Div, &a, &b));
348    finish(vm, r)
349}
350
351/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
352fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
353    let excluded = vm.pop();
354    let obj = vm.pop();
355    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
356        .unwrap_or_default()
357        .iter()
358        .map(|v| with_host(|h| h.str_of(v)))
359        .collect();
360    with_host(|h| {
361        let props: IndexMap<String, Value> = match h.get(&obj) {
362            Some(JsObj::Object(m)) => m
363                .iter()
364                .filter(|(k, _)| !excl.contains(k))
365                .map(|(k, v)| (k.clone(), v.clone()))
366                .collect(),
367            _ => IndexMap::new(),
368        };
369        h.new_object(props)
370    })
371}
372
373// ── helpers ──────────────────────────────────────────────────────────────────
374
375fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
376    let mut v = Vec::with_capacity(n);
377    for _ in 0..n {
378        v.push(vm.pop());
379    }
380    v.reverse();
381    v
382}
383
384/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
385fn sval(v: &Value) -> String {
386    if let Value::Str(s) = v {
387        return (**s).clone();
388    }
389    with_host(|h| h.as_str(v)).unwrap_or_default()
390}
391
392fn abort(vm: &mut VM, e: String) -> Value {
393    with_host(|h| h.error = Some(e));
394    vm.ip = vm.chunk.ops.len();
395    Value::Undef
396}
397
398/// Halt the chunk if a call left an error or non-local signal pending.
399fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
400    match r {
401        Ok(v) => {
402            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
403                vm.ip = vm.chunk.ops.len();
404            }
405            v
406        }
407        Err(e) => abort(vm, e),
408    }
409}
410
411// ── name handlers ─────────────────────────────────────────────────────────────
412
413fn b_getlocal(vm: &mut VM, _: u8) -> Value {
414    let name = sval(&vm.pop());
415    if let Some(v) = with_host(|h| h.read_name(&name)) {
416        return v;
417    }
418    // Globals bound lazily: numeric sentinels + builtin namespaces.
419    match name.as_str() {
420        "undefined" => return Value::Undef,
421        "NaN" => return Value::Float(f64::NAN),
422        "Infinity" => return Value::Float(f64::INFINITY),
423        "globalThis" => return with_host(|h| h.new_object(IndexMap::new())),
424        _ => {}
425    }
426    if is_namespace(&name) || is_known_builtin(&name) {
427        return with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
428    }
429    abort(vm, host::ref_error(&name))
430}
431
432fn b_setlocal(vm: &mut VM, _: u8) -> Value {
433    let val = vm.pop();
434    let name = sval(&vm.pop());
435    with_host(|h| h.set_name(&name, val.clone()));
436    val
437}
438
439fn b_declare(vm: &mut VM, _: u8) -> Value {
440    let val = vm.pop();
441    let name = sval(&vm.pop());
442    with_host(|h| h.declare_name(&name, val.clone()));
443    val
444}
445
446fn b_delname(vm: &mut VM, _: u8) -> Value {
447    let name = sval(&vm.pop());
448    with_host(|h| h.del_name(&name));
449    Value::Bool(true)
450}
451
452fn b_this(_vm: &mut VM, _: u8) -> Value {
453    with_host(|h| h.current_this().unwrap_or(Value::Undef))
454}
455
456fn b_load_null(_vm: &mut VM, _: u8) -> Value {
457    with_host(|h| h.null())
458}
459
460// ── attribute / item handlers ─────────────────────────────────────────────────
461
462fn b_getattr(vm: &mut VM, _: u8) -> Value {
463    let name = sval(&vm.pop());
464    let recv = vm.pop();
465    match get_property(&recv, &name) {
466        Ok(v) => v,
467        Err(e) => abort(vm, e),
468    }
469}
470
471/// Read `recv.name` (also the computed-key path for string keys). Walks own
472/// properties, accessors, and the prototype chain (class methods / getters).
473pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
474    if with_host(|h| h.is_nullish(recv)) {
475        return Err(host::type_error(&format!(
476            "Cannot read properties of {} (reading '{name}')",
477            with_host(|h| h.str_of(recv))
478        )));
479    }
480    // Accessor (own or inherited getter) takes precedence over the chain walk.
481    if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
482        return match getter {
483            Some(g) => host::invoke(&g, Vec::new(), Some(recv.clone())),
484            None => Ok(Value::Undef), // set-only property reads as undefined
485        };
486    }
487    // `constructor`: a user class/function sets it on the prototype chain, and
488    // that wins; otherwise every builtin instance reports its native
489    // constructor (so `[].constructor`, `new Map().constructor`,
490    // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
491    if name == "constructor" {
492        if let Some(v) = with_host(|h| {
493            match h.get(recv) {
494                Some(JsObj::Object(p)) => p.get("constructor").cloned(),
495                _ => None,
496            }
497            .or_else(|| host::lookup_chain(h, recv, "constructor"))
498        }) {
499            return Ok(v);
500        }
501        if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
502            return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
503        }
504    }
505    let obj = with_host(|h| h.get(recv).cloned());
506    Ok(match obj {
507        Some(JsObj::Object(props)) => {
508            // Typed-array element read (`ta[i]`): elements live in a hidden
509            // `@@elems`, not as own numeric props, so intercept integer keys.
510            if !name.is_empty()
511                && name.bytes().all(|b| b.is_ascii_digit())
512                && matches!(props.get("@@native"), Some(v) if with_host(|h| h.str_of(v)) == "TypedArray")
513            {
514                if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
515                    return Ok(v);
516                }
517            }
518            if let Some(v) = props.get(name) {
519                v.clone()
520            } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
521                // A method / data property inherited from the prototype chain.
522                v
523            } else if name == "__proto__" {
524                with_host(|h| h.proto_of(recv)).unwrap_or_else(|| with_host(|h| h.null()))
525            } else if crate::stdlib::native_tag(recv)
526                .map(|tag| crate::stdlib::instance_has_method(&tag, name))
527                .unwrap_or(false)
528            {
529                // A native instance method read as a property (`server.listen`) →
530                // a bound method, dispatched via `instance_call` when invoked.
531                bound_method(recv, name)
532            } else if is_object_method(name) {
533                bound_method(recv, name)
534            } else {
535                Value::Undef
536            }
537        }
538        Some(JsObj::Class(_)) | Some(JsObj::Func(_)) | Some(JsObj::BoundFunc { .. }) => {
539            function_property(recv, name)
540        }
541        Some(JsObj::Symbol { desc, .. }) => match name {
542            "description" => match desc {
543                Some(d) => with_host(|h| h.new_str(d)),
544                None => Value::Undef,
545            },
546            "toString" => bound_method(recv, name),
547            _ => Value::Undef,
548        },
549        Some(JsObj::BigInt(_)) => {
550            if matches!(
551                name,
552                "toString" | "valueOf" | "toLocaleString" | "constructor"
553            ) {
554                bound_method(recv, name)
555            } else {
556                Value::Undef
557            }
558        }
559        Some(JsObj::RegExp(r)) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
560            if crate::regexp::is_regexp_method(name) {
561                bound_method(recv, name)
562            } else {
563                Value::Undef
564            }
565        }),
566        Some(JsObj::Map { entries, .. }) => match name {
567            "size" => Value::Float(entries.len() as f64),
568            "@@iterator" => bound_method(recv, name),
569            _ if is_map_method(name) => bound_method(recv, name),
570            _ => Value::Undef,
571        },
572        Some(JsObj::Set { entries, .. }) => match name {
573            "size" => Value::Float(entries.len() as f64),
574            "@@iterator" => bound_method(recv, name),
575            _ if is_set_method(name) => bound_method(recv, name),
576            _ => Value::Undef,
577        },
578        Some(JsObj::Generator { .. }) => {
579            if is_generator_method(name) {
580                bound_method(recv, name)
581            } else {
582                Value::Undef
583            }
584        }
585        Some(JsObj::Promise { .. }) => {
586            if matches!(name, "then" | "catch" | "finally") {
587                bound_method(recv, name)
588            } else {
589                Value::Undef
590            }
591        }
592        Some(JsObj::Iter { .. }) => {
593            if matches!(name, "next" | "return" | "@@iterator") {
594                bound_method(recv, name)
595            } else {
596                Value::Undef
597            }
598        }
599        Some(JsObj::Array(items)) => {
600            if name == "length" {
601                Value::Float(items.len() as f64)
602            } else if let Ok(i) = name.parse::<usize>() {
603                items.get(i).cloned().unwrap_or(Value::Undef)
604            } else if name == "@@iterator" || is_array_method(name) || is_object_method(name) {
605                bound_method(recv, name)
606            } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
607                // Extra own props attached to an array (e.g. `RegExp.exec` result's
608                // `.index`/`.input`/`.groups`).
609                v
610            } else {
611                Value::Undef
612            }
613        }
614        Some(JsObj::Str(s)) => {
615            if name == "length" {
616                Value::Float(s.chars().count() as f64)
617            } else if let Ok(i) = name.parse::<usize>() {
618                match s.chars().nth(i) {
619                    Some(c) => with_host(|h| h.new_str(c.to_string())),
620                    None => Value::Undef,
621                }
622            } else if name == "@@iterator" || is_string_method(name) {
623                bound_method(recv, name)
624            } else {
625                Value::Undef
626            }
627        }
628        Some(JsObj::Builtin(ns)) => namespace_property(&ns, name),
629        _ => {
630            // Primitive numbers/booleans: method access -> bound method.
631            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
632                bound_method(recv, name)
633            } else {
634                Value::Undef
635            }
636        }
637    })
638}
639
640/// The builtin constructor name for a value with no own/inherited `constructor`
641/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
642/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
643/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
644/// whose `.constructor.name` is `""` in Node — not worth modelling).
645fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
646    match h.get(recv) {
647        Some(JsObj::Array(_)) => Some("Array"),
648        Some(JsObj::Object(props)) => {
649            // A native instance reports its own constructor, not Object — e.g.
650            // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
651            // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
652            // the `@@native` tag off the already-borrowed host (calling
653            // `native_tag`, which re-enters `with_host`, would double-borrow).
654            match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
655                Some("Buffer") => Some("Buffer"),
656                Some("URL") => Some("URL"),
657                Some("Date") => Some("Date"),
658                Some("WeakRef") => Some("WeakRef"),
659                Some("FinalizationRegistry") => Some("FinalizationRegistry"),
660                Some("TextEncoder") => Some("TextEncoder"),
661                Some("TextDecoder") => Some("TextDecoder"),
662                Some("EventEmitter") => Some("EventEmitter"),
663                _ => Some("Object"),
664            }
665        }
666        Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
667        Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
668        Some(JsObj::Promise { .. }) => Some("Promise"),
669        Some(JsObj::Str(_)) => Some("String"),
670        Some(JsObj::Symbol { .. }) => Some("Symbol"),
671        Some(JsObj::BigInt(_)) => Some("BigInt"),
672        Some(JsObj::RegExp(_)) => Some("RegExp"),
673        Some(JsObj::Iter { .. }) => Some("Iterator"),
674        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
675            Some("Function")
676        }
677        _ => match recv {
678            Value::Float(_) | Value::Int(_) => Some("Number"),
679            Value::Bool(_) => Some("Boolean"),
680            _ => None,
681        },
682    }
683}
684
685/// The builtin globals that are constructor *functions* (callable via `new`), so
686/// `Ctor.name` is the constructor name. Excludes the non-callable namespaces
687/// (`Math`, `JSON`, `console`, `Reflect`, `process`), whose `.name` is
688/// `undefined` in Node.
689fn is_builtin_ctor(name: &str) -> bool {
690    matches!(
691        name,
692        "Array"
693            | "Object"
694            | "Number"
695            | "String"
696            | "Boolean"
697            | "Symbol"
698            | "Function"
699            | "Map"
700            | "Set"
701            | "WeakMap"
702            | "WeakSet"
703            | "Promise"
704            | "BigInt"
705            | "Iterator"
706            | "RegExp"
707            | "Date"
708            | "ArrayBuffer"
709            | "Uint8Array"
710            | "Int8Array"
711            | "Uint8ClampedArray"
712            | "Int16Array"
713            | "Uint16Array"
714            | "Int32Array"
715            | "Uint32Array"
716            | "Float32Array"
717            | "Float64Array"
718            | "WeakRef"
719            | "FinalizationRegistry"
720            | "TextEncoder"
721            | "TextDecoder"
722            | "IncomingMessage"
723            | "ServerResponse"
724            | "EventEmitter"
725            | "Buffer"
726            | "URL"
727            | "URLSearchParams"
728    ) || host::ERROR_NAMES.contains(&name)
729}
730
731fn bound_method(recv: &Value, name: &str) -> Value {
732    with_host(|h| {
733        h.alloc(JsObj::BoundMethod {
734            recv: recv.clone(),
735            name: name.to_string(),
736        })
737    })
738}
739
740/// `Object.prototype` methods reachable on any object.
741fn is_object_method(name: &str) -> bool {
742    matches!(
743        name,
744        "hasOwnProperty"
745            | "isPrototypeOf"
746            | "propertyIsEnumerable"
747            | "toString"
748            | "valueOf"
749            | "constructor"
750    )
751}
752
753pub fn is_object_builtin_method(name: &str) -> bool {
754    matches!(
755        name,
756        "hasOwnProperty" | "isPrototypeOf" | "propertyIsEnumerable" | "toString" | "valueOf"
757    )
758}
759
760/// Dispatch an `Object.prototype` builtin method on an object/instance.
761pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
762    match name {
763        "hasOwnProperty" => {
764            let k = with_host(|h| h.property_key(&arg0(&args)));
765            // A builtin namespace/prototype receiver (`Map.prototype`) reports
766            // ownership via `has_property` (its methods resolve as thunks).
767            if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Builtin(_))) {
768                return Ok(Value::Bool(has_property(recv, &k)));
769            }
770            let has = with_host(|h| match h.get(recv) {
771                Some(JsObj::Object(p)) => p.contains_key(&k),
772                Some(JsObj::Array(items)) => {
773                    k == "length" || k.parse::<usize>().map(|i| i < items.len()).unwrap_or(false)
774                }
775                _ => false,
776            });
777            Ok(Value::Bool(has))
778        }
779        "isPrototypeOf" => {
780            let target = arg0(&args);
781            let mut cur = with_host(|h| h.proto_of(&target));
782            while let Some(p) = cur {
783                if with_host(|h| h.strict_eq(&p, recv)) {
784                    return Ok(Value::Bool(true));
785                }
786                cur = with_host(|h| h.proto_of(&p));
787            }
788            Ok(Value::Bool(false))
789        }
790        "propertyIsEnumerable" => {
791            let k = with_host(|h| h.str_of(&arg0(&args)));
792            let has =
793                with_host(|h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key(&k)));
794            Ok(Value::Bool(has))
795        }
796        "toString" => Ok(with_host(|h| {
797            // An instance with a custom `toString` up the chain is handled by
798            // call_method before reaching here; this is the default.
799            let s = h.str_of(recv);
800            h.new_str(s)
801        })),
802        "valueOf" => Ok(recv.clone()),
803        _ => Err(host::type_error(&format!("{name} is not a function"))),
804    }
805}
806
807/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
808/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
809/// these (so the caller can try statics).
810pub fn function_builtin_method(
811    recv: &Value,
812    name: &str,
813    args: &[Value],
814) -> Result<Option<Value>, String> {
815    match name {
816        "call" => {
817            let this = args.first().cloned();
818            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
819            Ok(Some(host::invoke(recv, rest, this)?))
820        }
821        "apply" => {
822            let this = args.first().cloned();
823            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
824            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
825                Vec::new()
826            } else {
827                with_host(|h| h.iter_vec(&arr)).unwrap_or_default()
828            };
829            Ok(Some(host::invoke(recv, call_args, this)?))
830        }
831        "bind" => {
832            let this = args.first().cloned().unwrap_or(Value::Undef);
833            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
834            Ok(Some(with_host(|h| {
835                h.alloc(JsObj::BoundFunc {
836                    target: recv.clone(),
837                    this,
838                    args: pre,
839                })
840            })))
841        }
842        "toString" => Ok(Some(with_host(|h| {
843            let s = h.str_of(recv);
844            h.new_str(s)
845        }))),
846        _ => Ok(None),
847    }
848}
849
850fn is_function_method(name: &str) -> bool {
851    matches!(name, "call" | "apply" | "bind" | "toString")
852}
853fn is_map_method(name: &str) -> bool {
854    matches!(
855        name,
856        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
857    )
858}
859fn is_set_method(name: &str) -> bool {
860    matches!(
861        name,
862        "add" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
863    )
864}
865fn is_generator_method(name: &str) -> bool {
866    matches!(name, "next" | "return" | "throw")
867}
868
869/// A property read on a function/class value: own fn-props (statics, name,
870/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
871fn function_property(recv: &Value, name: &str) -> Value {
872    // A class static, inherited down the constructor chain.
873    if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Class(_))) {
874        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
875            return v;
876        }
877    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
878        return v;
879    }
880    // A method inherited via the function's [[Prototype]] chain (set with
881    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
882    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
883    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
884        return v;
885    }
886    match name {
887        "name" => with_host(|h| {
888            let n = h.callable_name(recv);
889            h.new_str(n)
890        }),
891        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
892        "prototype" => ensure_fn_prototype(recv),
893        _ if is_function_method(name) => bound_method(recv, name),
894        _ => Value::Undef,
895    }
896}
897
898/// The `.prototype` of a function value, auto-created on first access (as Node
899/// does for every non-arrow function) with `.constructor` linking back. Arrow
900/// functions have no `prototype`.
901fn ensure_fn_prototype(recv: &Value) -> Value {
902    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
903        return p;
904    }
905    // Arrows / classes: no auto prototype (classes set their own).
906    let is_arrow =
907        matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Func(f)) if f.is_arrow);
908    if is_arrow {
909        return Value::Undef;
910    }
911    if !matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Func(_))) {
912        return Value::Undef;
913    }
914    with_host(|h| {
915        let proto = h.new_object(IndexMap::new());
916        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
917            p.insert("constructor".to_string(), recv.clone());
918        }
919        h.set_fn_prop(recv, "prototype", proto.clone());
920        proto
921    })
922}
923
924/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
925/// `console.log`).
926fn namespace_property(ns: &str, name: &str) -> Value {
927    // Numeric constants.
928    let konst = match (ns, name) {
929        ("Math", "PI") => Some(std::f64::consts::PI),
930        ("Math", "E") => Some(std::f64::consts::E),
931        ("Math", "LN2") => Some(std::f64::consts::LN_2),
932        ("Math", "LN10") => Some(std::f64::consts::LN_10),
933        ("Math", "LOG2E") => Some(std::f64::consts::LOG2_E),
934        ("Math", "LOG10E") => Some(std::f64::consts::LOG10_E),
935        ("Math", "SQRT2") => Some(std::f64::consts::SQRT_2),
936        ("Math", "SQRT1_2") => Some(std::f64::consts::FRAC_1_SQRT_2),
937        ("Number", "MAX_SAFE_INTEGER") => Some(9007199254740991.0),
938        ("Number", "MIN_SAFE_INTEGER") => Some(-9007199254740991.0),
939        ("Number", "MAX_VALUE") => Some(f64::MAX),
940        ("Number", "MIN_VALUE") => Some(f64::MIN_POSITIVE),
941        ("Number", "EPSILON") => Some(f64::EPSILON),
942        ("Number", "POSITIVE_INFINITY") => Some(f64::INFINITY),
943        ("Number", "NEGATIVE_INFINITY") => Some(f64::NEG_INFINITY),
944        ("Number", "NaN") => Some(f64::NAN),
945        _ => None,
946    };
947    if let Some(k) = konst {
948        return Value::Float(k);
949    }
950    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
951    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
952    // `undefined`.
953    if name == "name" && is_builtin_ctor(ns) {
954        return with_host(|h| h.new_str(ns.to_string()));
955    }
956    // The well-known `Symbol.iterator` symbol (used as a computed method key).
957    if ns == "Symbol" && name == "iterator" {
958        return with_host(|h| h.well_known_iterator());
959    }
960    if ns == "Symbol" && name == "asyncIterator" {
961        return with_host(|h| h.well_known_async_iterator());
962    }
963    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
964    // `buffer.Buffer`, `url.URL`).
965    if let Some(v) = crate::stdlib::constant(ns, name) {
966        return v;
967    }
968    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
969    // `Array.prototype`, …): a prototype namespace whose methods are callable
970    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
971    // `get-intrinsic`/`function-bind` family).
972    if name == "prototype" && is_builtin_ctor(ns) {
973        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
974    }
975    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
976    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
977    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
978    if let Some(ctor) = ns.strip_suffix(".prototype") {
979        return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
980    }
981    let qualified = format!("{ns}.{name}");
982    if is_known_builtin(&qualified) {
983        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
984    }
985    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
986    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
987        return v;
988    }
989    Value::Undef
990}
991
992/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
993/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
994/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
995/// libraries type-check on; every other method routes through normal method
996/// dispatch on `recv`.
997pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
998    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
999    if ctor == "Object" && method == "toString" {
1000        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
1001    }
1002    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
1003    // emitter method directly against `recv` (routing back through `call_method`
1004    // would re-resolve the mixed-in thunk and recurse).
1005    if ctor == "EventEmitter" {
1006        return crate::stdlib::events::instance_call(recv, method, args);
1007    }
1008    host::call_method(recv, method, args)
1009}
1010
1011/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
1012fn object_tag(h: &host::JsHost, v: &Value) -> String {
1013    let tag = match v {
1014        Value::Undef => "Undefined",
1015        Value::Bool(_) => "Boolean",
1016        Value::Int(_) | Value::Float(_) => "Number",
1017        Value::Str(_) => "String",
1018        Value::Obj(_) => match h.get(v) {
1019            Some(JsObj::Null) => "Null",
1020            Some(JsObj::Str(_)) => "String",
1021            Some(JsObj::Array(_)) => "Array",
1022            Some(JsObj::Func(_))
1023            | Some(JsObj::Class(_))
1024            | Some(JsObj::Builtin(_))
1025            | Some(JsObj::BoundFunc { .. })
1026            | Some(JsObj::BoundMethod { .. }) => "Function",
1027            Some(JsObj::RegExp(_)) => "RegExp",
1028            _ => "Object",
1029        },
1030        // node-js only produces the Value variants above; fusevm's shell-oriented
1031        // variants never arise here.
1032        _ => "Object",
1033    };
1034    format!("[object {tag}]")
1035}
1036
1037fn b_setattr(vm: &mut VM, _: u8) -> Value {
1038    let val = vm.pop();
1039    let name = sval(&vm.pop());
1040    let recv = vm.pop();
1041    set_property(&recv, &name, val.clone());
1042    val
1043}
1044
1045fn set_property(recv: &Value, name: &str, val: Value) {
1046    // `obj.__proto__ = p` re-links the prototype.
1047    if name == "__proto__" && matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Object(_)))
1048    {
1049        with_host(|h| h.set_proto(recv, val));
1050        return;
1051    }
1052    // An inherited/own setter accessor intercepts the write.
1053    if let Some((_, Some(setter))) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1054        let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
1055        return;
1056    }
1057    // A set-only-elsewhere getter (accessor with no setter): ignore the write.
1058    if let Some((Some(_), None)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1059        return;
1060    }
1061    // Writing `name`/`prototype`/statics on a function value.
1062    if matches!(
1063        with_host(|h| h.get(recv).cloned()),
1064        Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1065    ) {
1066        with_host(|h| h.set_fn_prop(recv, name, val));
1067        return;
1068    }
1069    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
1070    // Each bare reference is a fresh `Builtin` handle, so route to the stable
1071    // per-namespace side table rather than the per-index `fn_props`.
1072    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(recv).cloned()) {
1073        with_host(|h| h.set_builtin_static(&ns, name, val));
1074        return;
1075    }
1076    // `re.lastIndex = n` on a RegExp advances/resets its match cursor.
1077    if name == "lastIndex" {
1078        if let Some(n) = with_host(|h| match h.get(recv) {
1079            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
1080            _ => None,
1081        }) {
1082            with_host(|h| {
1083                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
1084                    r.last_index = if n.is_finite() && n >= 0.0 {
1085                        n as usize
1086                    } else {
1087                        0
1088                    };
1089                }
1090            });
1091            return;
1092        }
1093    }
1094    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
1095    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
1096        let is_ta = matches!(
1097            with_host(|h| h.get(recv).cloned()),
1098            Some(JsObj::Object(ref p)) if p.get("@@native").map(|v| with_host(|h| h.str_of(v))).as_deref() == Some("TypedArray")
1099        );
1100        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val) {
1101            return;
1102        }
1103    }
1104    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
1105    if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Array(_)))
1106        && name != "length"
1107        && name.parse::<usize>().is_err()
1108    {
1109        with_host(|h| h.set_fn_prop(recv, name, val));
1110        return;
1111    }
1112    with_host(|h| match h.get_mut(recv) {
1113        Some(JsObj::Object(props)) => {
1114            // Adding a *new* array-index key must re-place it into ascending
1115            // integer-key order (updating an existing key keeps its position).
1116            let is_new = !props.contains_key(name);
1117            props.insert(name.to_string(), val);
1118            if is_new && host::array_index(name).is_some() {
1119                host::canonicalize_own_keys(props);
1120            }
1121        }
1122        Some(JsObj::Array(items)) => {
1123            if name == "length" {
1124                let n = h_val_to_len(&val);
1125                items.resize(n, Value::Undef);
1126            } else if let Ok(i) = name.parse::<usize>() {
1127                if i >= items.len() {
1128                    items.resize(i + 1, Value::Undef);
1129                }
1130                items[i] = val;
1131            }
1132        }
1133        _ => {}
1134    });
1135}
1136
1137fn h_val_to_len(v: &Value) -> usize {
1138    match v {
1139        Value::Float(f) if f.is_finite() && *f >= 0.0 => *f as usize,
1140        Value::Int(n) if *n >= 0 => *n as usize,
1141        _ => 0,
1142    }
1143}
1144
1145fn b_getitem(vm: &mut VM, _: u8) -> Value {
1146    let idx = vm.pop();
1147    let recv = vm.pop();
1148    let key = with_host(|h| h.property_key(&idx));
1149    match get_property(&recv, &key) {
1150        Ok(v) => v,
1151        Err(e) => abort(vm, e),
1152    }
1153}
1154
1155fn b_setitem(vm: &mut VM, _: u8) -> Value {
1156    let val = vm.pop();
1157    let idx = vm.pop();
1158    let recv = vm.pop();
1159    let key = with_host(|h| h.property_key(&idx));
1160    set_property(&recv, &key, val.clone());
1161    val
1162}
1163
1164fn b_delitem(vm: &mut VM, _: u8) -> Value {
1165    let idx = vm.pop();
1166    let recv = vm.pop();
1167    let key = with_host(|h| h.str_of(&idx));
1168    with_host(|h| match h.get_mut(&recv) {
1169        Some(JsObj::Object(props)) => {
1170            props.shift_remove(&key);
1171        }
1172        Some(JsObj::Array(items)) => {
1173            if let Ok(i) = key.parse::<usize>() {
1174                if i < items.len() {
1175                    items[i] = Value::Undef;
1176                }
1177            }
1178        }
1179        _ => {}
1180    });
1181    Value::Bool(true)
1182}
1183
1184fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
1185    let name = sval(&vm.pop());
1186    let recv = vm.pop();
1187    with_host(|h| {
1188        if let Some(JsObj::Object(props)) = h.get_mut(&recv) {
1189            props.shift_remove(&name);
1190        }
1191    });
1192    Value::Bool(true)
1193}
1194
1195// ── constructors ──────────────────────────────────────────────────────────────
1196
1197fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
1198    let parts = pop_n(vm, argc as usize);
1199    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
1200    with_host(|h| h.new_str(s))
1201}
1202
1203fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
1204    let items = pop_n(vm, argc as usize);
1205    with_host(|h| h.new_array(items))
1206}
1207
1208fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
1209    let flat = pop_n(vm, argc as usize);
1210    let mut props: IndexMap<String, Value> = IndexMap::new();
1211    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
1212    let mut proto_override: Option<Value> = None;
1213    let mut i = 0;
1214    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
1215        if i + 2 >= flat.len() {
1216            break;
1217        }
1218        let spread = matches!(flat[i], Value::Int(1));
1219        if spread {
1220            let src = flat[i + 1].clone();
1221            let entries = with_host(|h| match h.get(&src) {
1222                Some(JsObj::Object(m)) => m
1223                    .iter()
1224                    .map(|(k, v)| (k.clone(), v.clone()))
1225                    .collect::<Vec<_>>(),
1226                Some(JsObj::Array(items)) => items
1227                    .iter()
1228                    .enumerate()
1229                    .map(|(idx, v)| (idx.to_string(), v.clone()))
1230                    .collect::<Vec<_>>(),
1231                _ => Vec::new(),
1232            });
1233            for (k, v) in entries {
1234                props.insert(k, v);
1235            }
1236        } else {
1237            let key = with_host(|h| h.str_of(&flat[i + 1]));
1238            if key == "__proto__" {
1239                proto_override = Some(flat[i + 2].clone());
1240            } else {
1241                props.insert(key, flat[i + 2].clone());
1242            }
1243        }
1244        i += 3;
1245    }
1246    with_host(|h| {
1247        let o = h.new_object(props);
1248        if let Some(p) = proto_override {
1249            if matches!(p, Value::Obj(_)) {
1250                h.set_proto(&o, p);
1251            }
1252        }
1253        o
1254    })
1255}
1256
1257fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
1258    let def_id = match vm.pop() {
1259        Value::Int(n) => n as usize,
1260        Value::Float(f) => f as usize,
1261        _ => return abort(vm, "internal: MKFUNC id".into()),
1262    };
1263    let is_arrow = with_host(|h| h.funcs.get(def_id).map(|d| d.is_arrow).unwrap_or(false));
1264    with_host(|h| {
1265        let env = h.current_env_capture();
1266        let this = h.current_this();
1267        h.alloc(JsObj::Func(FuncVal {
1268            def_id,
1269            env: Some(env),
1270            this,
1271            is_arrow,
1272            home_class: None,
1273        }))
1274    })
1275}
1276
1277// ── truthiness / coercion / equality ──────────────────────────────────────────
1278
1279fn b_truthy(vm: &mut VM, _: u8) -> Value {
1280    let v = vm.pop();
1281    Value::Bool(with_host(|h| h.truthy(&v)))
1282}
1283
1284fn b_nullish(vm: &mut VM, _: u8) -> Value {
1285    let v = vm.pop();
1286    Value::Bool(with_host(|h| h.is_nullish(&v)))
1287}
1288
1289fn b_tostr(vm: &mut VM, _: u8) -> Value {
1290    let v = vm.pop();
1291    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
1292    // `String(x)`, object keys).
1293    match host::to_string_value(&v) {
1294        Ok(s) => s,
1295        Err(e) => abort(vm, e),
1296    }
1297}
1298
1299fn b_typeof(vm: &mut VM, _: u8) -> Value {
1300    let v = vm.pop();
1301    with_host(|h| {
1302        let t = h.type_of(&v);
1303        h.new_str(t)
1304    })
1305}
1306
1307/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
1308/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
1309fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
1310    let name = sval(&vm.pop());
1311    // Bound name (user variable) → typeof its value.
1312    if let Some(v) = with_host(|h| h.read_name(&name)) {
1313        return with_host(|h| {
1314            let t = h.type_of(&v);
1315            h.new_str(t)
1316        });
1317    }
1318    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
1319    // would produce, then take its type (so object-namespaces like `console`/
1320    // `Math`/`JSON`/`process` report "object", constructors report "function").
1321    let t = match name.as_str() {
1322        "undefined" => "undefined".to_string(),
1323        "NaN" | "Infinity" => "number".to_string(),
1324        "globalThis" => "object".to_string(),
1325        n if is_namespace(n) || is_known_builtin(n) => {
1326            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
1327            with_host(|h| h.type_of(&v)).to_string()
1328        }
1329        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
1330    };
1331    with_host(|h| h.new_str(t))
1332}
1333
1334fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
1335    let b = vm.pop();
1336    let a = vm.pop();
1337    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
1338}
1339
1340fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
1341    let b = vm.pop();
1342    let a = vm.pop();
1343    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
1344}
1345
1346fn b_instanceof(vm: &mut VM, _: u8) -> Value {
1347    let ctor = vm.pop();
1348    let obj = vm.pop();
1349    match host::instance_of(&obj, &ctor) {
1350        Ok(b) => Value::Bool(b),
1351        Err(e) => abort(vm, e),
1352    }
1353}
1354
1355// ── bitwise / unary ───────────────────────────────────────────────────────────
1356
1357fn b_binop(vm: &mut VM, _: u8) -> Value {
1358    let b = vm.pop();
1359    let a = vm.pop();
1360    let tag = match vm.pop() {
1361        Value::Int(n) => n,
1362        _ => 0,
1363    };
1364    let r = with_host(|h| h.bitwise(tag, &a, &b));
1365    finish(vm, r)
1366}
1367
1368fn b_unary(vm: &mut VM, _: u8) -> Value {
1369    let v = vm.pop();
1370    let tag = match vm.pop() {
1371        Value::Int(n) => n,
1372        _ => 0,
1373    };
1374    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
1375    // computed in arbitrary precision.
1376    if with_host(|h| h.is_bigint_val(&v)) {
1377        return match tag {
1378            host::unop::POS => abort(
1379                vm,
1380                host::type_error("Cannot convert a BigInt value to a number"),
1381            ),
1382            host::unop::BITNOT => {
1383                let b = with_host(|h| h.as_bigint(&v)).unwrap();
1384                let r = -(b + num_bigint::BigInt::from(1));
1385                with_host(|h| h.new_bigint(r))
1386            }
1387            _ => Value::Undef,
1388        };
1389    }
1390    with_host(|h| match tag {
1391        host::unop::POS => Value::Float(h.to_number(&v)),
1392        host::unop::BITNOT => {
1393            let n = h.to_number(&v);
1394            let i = if n.is_finite() {
1395                n.trunc() as i64 as i32
1396            } else {
1397                0
1398            };
1399            Value::Float(!i as f64)
1400        }
1401        _ => Value::Undef,
1402    })
1403}
1404
1405// ── membership ────────────────────────────────────────────────────────────────
1406
1407fn b_contains(vm: &mut VM, _: u8) -> Value {
1408    let container = vm.pop();
1409    let key = vm.pop();
1410    // `x in y` requires y to be an object.
1411    if !matches!(container, Value::Obj(_)) {
1412        return abort(vm, host::type_error("Cannot use 'in' operator to search"));
1413    }
1414    let k = with_host(|h| h.property_key(&key));
1415    Value::Bool(has_property(&container, &k))
1416}
1417
1418// ── control ───────────────────────────────────────────────────────────────────
1419
1420fn b_sig_return(vm: &mut VM, _: u8) -> Value {
1421    let v = vm.pop();
1422    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
1423    vm.ip = vm.chunk.ops.len();
1424    v
1425}
1426
1427fn b_throw(vm: &mut VM, _: u8) -> Value {
1428    let v = vm.pop();
1429    let msg = with_host(|h| {
1430        h.exc = Some(v.clone());
1431        // Prefer an error object's message for the top-level report.
1432        error_display(h, &v)
1433    });
1434    abort(vm, msg)
1435}
1436
1437fn error_display(h: &host::JsHost, v: &Value) -> String {
1438    if let Some(JsObj::Object(props)) = h.get(v) {
1439        let name = props
1440            .get("name")
1441            .map(|x| h.str_of(x))
1442            .unwrap_or_else(|| "Error".into());
1443        if let Some(m) = props.get("message") {
1444            return format!("Uncaught {name}: {}", h.str_of(m));
1445        }
1446    }
1447    format!("Uncaught {}", h.str_of(v))
1448}
1449
1450fn b_try(vm: &mut VM, _: u8) -> Value {
1451    let id = match vm.pop() {
1452        Value::Int(n) => n as usize,
1453        _ => return abort(vm, "internal: TRY id".into()),
1454    };
1455    let td = match with_host(|h| h.try_def(id)) {
1456        Some(t) => t,
1457        None => return abort(vm, "internal: unknown try id".into()),
1458    };
1459    let mut pending: Option<String> = None;
1460
1461    let body_res = host::run_chunk_on(td.block.clone());
1462    let signal_after = with_host(|h| h.signal.is_some());
1463    if let Err(e) = body_res {
1464        if signal_after {
1465            pending = Some(e);
1466        } else if let Some((bind, hbody)) = &td.handler {
1467            // Bind the thrown value (or a synthesized error) to the catch param.
1468            let thrown =
1469                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
1470            with_host(|h| {
1471                h.error = None;
1472                h.exc = None;
1473            });
1474            if let Some(name) = bind {
1475                with_host(|h| h.declare_name(name, thrown));
1476            }
1477            if let Err(e2) = host::run_chunk_on(hbody.clone()) {
1478                pending = Some(e2);
1479            }
1480        } else {
1481            pending = Some(e);
1482        }
1483    }
1484
1485    // finally always runs; a finally error/signal supersedes.
1486    if let Some(fin) = &td.finalizer {
1487        let sig_before = with_host(|h| h.signal.take());
1488        match host::run_chunk_on(fin.clone()) {
1489            Ok(_) => {
1490                if with_host(|h| h.signal.is_none()) {
1491                    with_host(|h| h.signal = sig_before);
1492                }
1493            }
1494            Err(e) => pending = Some(e),
1495        }
1496    }
1497
1498    if let Some(e) = pending {
1499        return abort(vm, e);
1500    }
1501    Value::Undef
1502}
1503
1504/// Synthesize an `Error`-shaped object from an internal error string, linked to
1505/// the matching builtin error prototype so `instanceof`/`.constructor` work.
1506pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
1507    h.ensure_error_protos();
1508    let (name, message) = match e.split_once(": ") {
1509        Some((n, m)) if host::ERROR_NAMES.contains(&n) => (n.to_string(), m.to_string()),
1510        _ => ("Error".to_string(), e.to_string()),
1511    };
1512    let mut props: IndexMap<String, Value> = IndexMap::new();
1513    let mv = h.new_str(message.clone());
1514    props.insert("message".into(), mv);
1515    let stack = if message.is_empty() {
1516        format!("{name}\n    at <anonymous>")
1517    } else {
1518        format!("{name}: {message}\n    at <anonymous>")
1519    };
1520    let sv = h.new_str(stack);
1521    props.insert("stack".into(), sv);
1522    let obj = h.new_object(props);
1523    if let Some(p) = host::error_proto_of(h, &name) {
1524        h.set_proto(&obj, p);
1525    }
1526    obj
1527}
1528
1529// ── iteration ─────────────────────────────────────────────────────────────────
1530
1531fn b_getiter(vm: &mut VM, _: u8) -> Value {
1532    let v = vm.pop();
1533    // A generator is its own iterator (resumed lazily by FORITER).
1534    if with_host(|h| h.is_generator_val(&v)) {
1535        return v;
1536    }
1537    // An object with a user `Symbol.iterator`: call it to get the iterator object.
1538    if let Some(iter_fn) = with_host(|h| host::lookup_chain(h, &v, "@@iterator")) {
1539        if with_host(|h| host::is_callable(h, &iter_fn)) {
1540            return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
1541                Ok(it) => it,
1542                Err(e) => abort(vm, e),
1543            };
1544        }
1545    }
1546    match with_host(|h| h.iter_vec(&v)) {
1547        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
1548        Err(e) => abort(vm, e),
1549    }
1550}
1551
1552fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
1553    let v = vm.pop();
1554    let keys = with_host(|h| h.enum_keys(&v));
1555    with_host(|h| h.new_array(keys))
1556}
1557
1558fn b_foriter(vm: &mut VM, _: u8) -> Value {
1559    let it = match vm.stack.last() {
1560        Some(v) => v.clone(),
1561        None => return abort(vm, "internal: FORITER with empty stack".into()),
1562    };
1563    // Eager array-backed iterator (arrays/strings/Map/Set).
1564    let eager = with_host(|h| {
1565        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
1566            if *idx < items.len() {
1567                let v = items[*idx].clone();
1568                *idx += 1;
1569                return Some(Some(v));
1570            }
1571            return Some(None);
1572        }
1573        None
1574    });
1575    if let Some(step) = eager {
1576        return match step {
1577            Some(v) => {
1578                vm.push(v);
1579                Value::Bool(true)
1580            }
1581            None => Value::Bool(false),
1582        };
1583    }
1584    // Generator: resume one step.
1585    if with_host(|h| h.is_generator_val(&it)) {
1586        return match host::gen_resume(&it, Value::Undef) {
1587            Ok(host::GenStep::Yield(v)) => {
1588                vm.push(v);
1589                Value::Bool(true)
1590            }
1591            Ok(host::GenStep::Done(_)) => Value::Bool(false),
1592            Err(e) => abort(vm, e),
1593        };
1594    }
1595    // A user iterator object with a `.next()` returning `{ value, done }`.
1596    match host::call_method(&it, "next", Vec::new()) {
1597        Ok(step) => {
1598            let done = get_property(&step, "done")
1599                .map(|d| with_host(|h| h.truthy(&d)))
1600                .unwrap_or(true);
1601            if done {
1602                Value::Bool(false)
1603            } else {
1604                match get_property(&step, "value") {
1605                    Ok(v) => {
1606                        vm.push(v);
1607                        Value::Bool(true)
1608                    }
1609                    Err(e) => abort(vm, e),
1610                }
1611            }
1612        }
1613        Err(e) => abort(vm, e),
1614    }
1615}
1616
1617fn b_unpack(vm: &mut VM, _: u8) -> Value {
1618    let star = match vm.pop() {
1619        Value::Int(n) => n,
1620        _ => -1,
1621    };
1622    let count = match vm.pop() {
1623        Value::Int(n) => n as usize,
1624        _ => 0,
1625    };
1626    let iterable = vm.pop();
1627    let items = match host::iter_all(&iterable) {
1628        Ok(v) => v,
1629        Err(e) => return abort(vm, e),
1630    };
1631    let ordered: Vec<Value> = if star < 0 {
1632        (0..count)
1633            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
1634            .collect()
1635    } else {
1636        let si = star as usize;
1637        let after = count.saturating_sub(si + 1);
1638        let rest_end = items.len().saturating_sub(after).max(si);
1639        let mut out: Vec<Value> = Vec::with_capacity(count);
1640        for i in 0..si {
1641            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
1642        }
1643        let rest: Vec<Value> = items
1644            .get(si..rest_end)
1645            .map(|s| s.to_vec())
1646            .unwrap_or_default();
1647        out.push(with_host(|h| h.new_array(rest)));
1648        for j in 0..after {
1649            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
1650        }
1651        out
1652    };
1653    if ordered.is_empty() {
1654        return Value::Undef;
1655    }
1656    for it in ordered[1..].iter().rev().cloned() {
1657        vm.push(it);
1658    }
1659    ordered[0].clone()
1660}
1661
1662fn b_build_args(vm: &mut VM, argc: u8) -> Value {
1663    let flat = pop_n(vm, argc as usize);
1664    let mut out = Vec::new();
1665    let mut i = 0;
1666    while i + 1 < flat.len() {
1667        let spread = matches!(flat[i], Value::Int(1));
1668        let val = flat[i + 1].clone();
1669        if spread {
1670            match host::iter_all(&val) {
1671                Ok(items) => out.extend(items),
1672                Err(e) => return abort(vm, e),
1673            }
1674        } else {
1675            out.push(val);
1676        }
1677        i += 2;
1678    }
1679    with_host(|h| h.new_array(out))
1680}
1681
1682// ── calls ──────────────────────────────────────────────────────────────────────
1683
1684fn b_call(vm: &mut VM, argc: u8) -> Value {
1685    let mut args = pop_n(vm, argc as usize);
1686    let name = sval(&args.remove(0));
1687    let r = host::call_named(&name, args);
1688    finish(vm, r)
1689}
1690
1691fn b_call_method(vm: &mut VM, argc: u8) -> Value {
1692    let mut args = pop_n(vm, argc as usize);
1693    let recv = args.remove(0);
1694    let name = sval(&args.remove(0));
1695    let r = host::call_method(&recv, &name, args);
1696    finish(vm, r)
1697}
1698
1699fn b_call_value(vm: &mut VM, argc: u8) -> Value {
1700    let mut args = pop_n(vm, argc as usize);
1701    let callable = args.remove(0);
1702    let r = host::invoke(&callable, args, None);
1703    finish(vm, r)
1704}
1705
1706fn b_new(vm: &mut VM, argc: u8) -> Value {
1707    let mut args = pop_n(vm, argc as usize);
1708    let ctor = args.remove(0);
1709    let r = host::construct(&ctor, args);
1710    finish(vm, r)
1711}
1712
1713fn b_apply(vm: &mut VM, _: u8) -> Value {
1714    let args_arr = vm.pop();
1715    let callable = vm.pop();
1716    let args = host::iter_all(&args_arr).unwrap_or_default();
1717    let r = host::invoke(&callable, args, None);
1718    finish(vm, r)
1719}
1720
1721fn b_apply_method(vm: &mut VM, _: u8) -> Value {
1722    let args_arr = vm.pop();
1723    let name = sval(&vm.pop());
1724    let recv = vm.pop();
1725    let args = host::iter_all(&args_arr).unwrap_or_default();
1726    let r = host::call_method(&recv, &name, args);
1727    finish(vm, r)
1728}
1729
1730// ── numeric hook ──────────────────────────────────────────────────────────────
1731
1732/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
1733/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
1734pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
1735    with_host(|h| h.arith(op, a, b))
1736}
1737
1738// ══ standard library ═══════════════════════════════════════════════════════════
1739
1740/// Namespaces reachable as bare globals.
1741fn is_namespace(name: &str) -> bool {
1742    matches!(
1743        name,
1744        "console"
1745            | "Math"
1746            | "JSON"
1747            | "Object"
1748            | "Array"
1749            | "Number"
1750            | "String"
1751            | "Boolean"
1752            | "Symbol"
1753            | "Reflect"
1754            | "Promise"
1755            | "process"
1756            | "Buffer"
1757            | "URL"
1758            | "URLSearchParams"
1759    )
1760}
1761
1762const GLOBAL_FUNCS: &[&str] = &[
1763    "parseInt",
1764    "parseFloat",
1765    "isNaN",
1766    "isFinite",
1767    "encodeURIComponent",
1768    "decodeURIComponent",
1769    "encodeURI",
1770    "decodeURI",
1771    "eval",
1772    "String",
1773    "Number",
1774    "Boolean",
1775    "Array",
1776    "Object",
1777    "Function",
1778    "Symbol",
1779    "Map",
1780    "Set",
1781    "WeakMap",
1782    "WeakSet",
1783    "Promise",
1784    "Error",
1785    "TypeError",
1786    "RangeError",
1787    "SyntaxError",
1788    "ReferenceError",
1789    "EvalError",
1790    "URIError",
1791    "BigInt",
1792    "RegExp",
1793    "Date",
1794    "ArrayBuffer",
1795    "Uint8Array",
1796    "Int8Array",
1797    "Uint8ClampedArray",
1798    "Int16Array",
1799    "Uint16Array",
1800    "Int32Array",
1801    "Uint32Array",
1802    "Float32Array",
1803    "Float64Array",
1804    "WeakRef",
1805    "FinalizationRegistry",
1806    "TextEncoder",
1807    "TextDecoder",
1808    "queueMicrotask",
1809    "setTimeout",
1810    "setInterval",
1811    "setImmediate",
1812    "clearTimeout",
1813    "clearInterval",
1814    "structuredClone",
1815    "require",
1816    // CommonJS loader dispatch targets referenced by per-module `require`
1817    // closures (see `module.rs`); never written by user code.
1818    "__cjs_require",
1819    "__cjs_resolve",
1820];
1821
1822const NS_METHODS: &[&str] = &[
1823    "console.log",
1824    "console.error",
1825    "console.warn",
1826    "console.info",
1827    "console.debug",
1828    "Math.floor",
1829    "Math.ceil",
1830    "Math.round",
1831    "Math.trunc",
1832    "Math.abs",
1833    "Math.sign",
1834    "Math.max",
1835    "Math.min",
1836    "Math.pow",
1837    "Math.sqrt",
1838    "Math.cbrt",
1839    "Math.random",
1840    "Math.hypot",
1841    "Math.clz32",
1842    "Math.fround",
1843    "Math.log",
1844    "Math.log2",
1845    "Math.log10",
1846    "Math.exp",
1847    "Math.sin",
1848    "Math.cos",
1849    "Math.tan",
1850    "Math.atan",
1851    "Math.atan2",
1852    "Math.asin",
1853    "Math.acos",
1854    "JSON.stringify",
1855    "JSON.parse",
1856    "Object.keys",
1857    "Object.values",
1858    "Object.entries",
1859    "Object.assign",
1860    "Object.freeze",
1861    "Object.is",
1862    "Object.fromEntries",
1863    "Object.getPrototypeOf",
1864    "Object.setPrototypeOf",
1865    "Object.create",
1866    "Object.getOwnPropertyNames",
1867    "Object.defineProperty",
1868    "Object.getOwnPropertyDescriptor",
1869    "Object.hasOwn",
1870    "Object.groupBy",
1871    "Array.isArray",
1872    "Array.from",
1873    "Array.of",
1874    "Number.isInteger",
1875    "Number.isNaN",
1876    "Number.isFinite",
1877    "Number.isSafeInteger",
1878    "Number.parseInt",
1879    "Number.parseFloat",
1880    "String.fromCharCode",
1881    "String.fromCodePoint",
1882    "String.raw",
1883    "Symbol.for",
1884    "Symbol.keyFor",
1885    "BigInt.asIntN",
1886    "BigInt.asUintN",
1887    "Reflect.ownKeys",
1888    "Reflect.has",
1889    "Reflect.get",
1890    "Reflect.set",
1891    "Reflect.getPrototypeOf",
1892    "Promise.resolve",
1893    "Promise.reject",
1894    "Promise.all",
1895    "Promise.allSettled",
1896    "Promise.race",
1897    "Promise.any",
1898    "Promise.withResolvers",
1899    "Map.groupBy",
1900    "process.nextTick",
1901    "Error.captureStackTrace",
1902    "require.resolve",
1903];
1904
1905pub fn is_known_builtin(name: &str) -> bool {
1906    GLOBAL_FUNCS.contains(&name)
1907        || NS_METHODS.contains(&name)
1908        || is_namespace(name)
1909        || crate::stdlib::is_method(name)
1910}
1911
1912/// Call a resolved builtin function (global or `namespace.method`).
1913pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
1914    // `require(spec)`: the ENTRY script's top-level require — core module first,
1915    // else the CommonJS loader resolving from the entry file's directory.
1916    if name == "require" {
1917        let spec = with_host(|h| h.str_of(&arg0(&args)));
1918        return crate::module::require(&spec, &crate::module::entry_dir());
1919    }
1920    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
1921    // into the loader, resolving `spec` against the module's own directory.
1922    if name == "__cjs_require" {
1923        let spec = with_host(|h| h.str_of(&arg0(&args)));
1924        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
1925        return crate::module::require(&spec, std::path::Path::new(&from));
1926    }
1927    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
1928    if name == "require.resolve" {
1929        let spec = with_host(|h| h.str_of(&arg0(&args)));
1930        if crate::stdlib::resolve(&spec).is_some() {
1931            return Ok(with_host(|h| h.new_str(spec)));
1932        }
1933        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
1934            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
1935            None => Err(format!("Error: Cannot find module '{spec}'")),
1936        };
1937    }
1938    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
1939    // path (core modules resolve to the bare specifier, as in Node).
1940    if name == "__cjs_resolve" {
1941        let spec = with_host(|h| h.str_of(&arg0(&args)));
1942        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
1943        if crate::stdlib::resolve(&spec).is_some() {
1944            return Ok(with_host(|h| h.new_str(spec)));
1945        }
1946        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
1947            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
1948            None => Err(format!("Error: Cannot find module '{spec}'")),
1949        };
1950    }
1951    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
1952    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
1953    // stack-introspection pattern used by `depd`), it is called with a synthetic
1954    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
1955    if name == "Error.captureStackTrace" {
1956        let target = arg0(&args);
1957        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1958        let stack = match prep {
1959            Some(f)
1960                if matches!(
1961                    with_host(|h| h.get(&f).cloned()),
1962                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1963                ) =>
1964            {
1965                let sites = crate::module::callsite_stack(10)?;
1966                host::invoke(&f, vec![target.clone(), sites], None)?
1967            }
1968            _ => with_host(|h| h.new_str("")),
1969        };
1970        set_property(&target, "stack", stack);
1971        return Ok(Value::Undef);
1972    }
1973    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
1974    if let Some(r) = crate::stdlib::call(name, &args) {
1975        return r;
1976    }
1977    match name {
1978        "console.log" | "console.info" | "console.debug" => {
1979            print_line(&args, false);
1980            Ok(Value::Undef)
1981        }
1982        "console.error" | "console.warn" => {
1983            print_line(&args, true);
1984            Ok(Value::Undef)
1985        }
1986        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args))),
1987        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args))),
1988        "isNaN" => Ok(Value::Bool(arg_num(&args, 0).is_nan())),
1989        "isFinite" => Ok(Value::Bool(arg_num(&args, 0).is_finite())),
1990        "encodeURIComponent" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), false),
1991        "encodeURI" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), true),
1992        "decodeURIComponent" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), false),
1993        "decodeURI" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), true),
1994        // `eval` exists as a global (libraries like get-intrinsic capture it as an
1995        // intrinsic) but node-js has no runtime source evaluator; calling it is
1996        // unsupported. A non-string argument is returned unchanged, as in JS.
1997        "eval" => match arg0(&args) {
1998            v @ (Value::Undef | Value::Bool(_) | Value::Int(_) | Value::Float(_)) => Ok(v),
1999            _ => Err(host::type_error("eval is not supported in node-js")),
2000        },
2001        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
2002        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
2003        "Number.isNaN" => Ok(Value::Bool(
2004            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
2005        )),
2006        "Number.isFinite" => Ok(Value::Bool(
2007            matches!(arg0(&args), Value::Float(f) if f.is_finite())
2008                || matches!(arg0(&args), Value::Int(_)),
2009        )),
2010        "String" => {
2011            if args.is_empty() {
2012                Ok(with_host(|h| h.new_str("")))
2013            } else {
2014                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
2015                // is allowed); everything else via ToString method dispatch.
2016                host::to_string_value(&args[0])
2017            }
2018        }
2019        "Number" => Ok(Value::Float(if args.is_empty() {
2020            0.0
2021        } else {
2022            with_host(|h| h.to_number(&args[0]))
2023        })),
2024        "BigInt" => bigint_ctor(&arg0(&args)),
2025        "RegExp" => regexp_ctor(&args),
2026        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
2027        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
2028        "String.fromCharCode" => Ok(with_host(|h| {
2029            let s: String = args
2030                .iter()
2031                .filter_map(|a| char::from_u32(h.to_number(a) as u32))
2032                .collect();
2033            h.new_str(s)
2034        })),
2035        "String.raw" => string_raw(&args),
2036        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
2037        "Array" => construct_builtin("Array", args),
2038        "Array.of" => Ok(with_host(|h| h.new_array(args))),
2039        "Array.isArray" => Ok(Value::Bool(matches!(
2040            with_host(|h| h.get(&arg0(&args)).cloned()),
2041            Some(JsObj::Array(_))
2042        ))),
2043        "Array.from" => array_from(args),
2044        "Object" => Ok(object_call(args)),
2045        "Object.keys" => object_keys(args, 0),
2046        "Object.values" => object_keys(args, 1),
2047        "Object.entries" => object_keys(args, 2),
2048        "Object.assign" => object_assign(args),
2049        "Object.freeze" => Ok(arg0(&args)),
2050        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
2051        // distinct from -0.
2052        "Object.is" => {
2053            let a = arg0(&args);
2054            let b = args.get(1).cloned().unwrap_or(Value::Undef);
2055            let num = |v: &Value| match v {
2056                Value::Int(n) => Some(*n as f64),
2057                Value::Float(f) => Some(*f),
2058                _ => None,
2059            };
2060            let r = match (num(&a), num(&b)) {
2061                (Some(x), Some(y)) => {
2062                    if x.is_nan() && y.is_nan() {
2063                        true
2064                    } else if x == 0.0 && y == 0.0 {
2065                        x.is_sign_negative() == y.is_sign_negative()
2066                    } else {
2067                        x == y
2068                    }
2069                }
2070                _ => with_host(|h| h.strict_eq(&a, &b)),
2071            };
2072            Ok(Value::Bool(r))
2073        }
2074        "Object.fromEntries" => object_from_entries(args),
2075        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => Ok(with_host(|h| {
2076            h.proto_of(&arg0(&args)).unwrap_or_else(|| h.null())
2077        })),
2078        "Object.setPrototypeOf" => {
2079            let obj = arg0(&args);
2080            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
2081            with_host(|h| h.set_proto(&obj, proto));
2082            Ok(obj)
2083        }
2084        "Object.create" => object_create(args),
2085        "Object.getOwnPropertyNames" => object_keys(args, 0),
2086        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
2087        "Object.hasOwn" => {
2088            let obj = arg0(&args);
2089            let key = args.get(1).cloned().unwrap_or(Value::Undef);
2090            object_builtin_method(&obj, "hasOwnProperty", vec![key])
2091        }
2092        "Object.defineProperty" => object_define_property(args),
2093        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
2094        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
2095        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
2096        "Object.groupBy" => object_group_by(args),
2097        "Symbol" => Ok(with_host(|h| {
2098            let desc = args
2099                .first()
2100                .filter(|a| !matches!(a, Value::Undef))
2101                .map(|a| h.str_of(a));
2102            h.new_symbol(desc)
2103        })),
2104        "Symbol.for" => Ok(with_host(|h| {
2105            let key = h.str_of(&arg0(&args));
2106            h.symbol_for(&key)
2107        })),
2108        "Symbol.keyFor" => Ok(with_host(|h| match h.get(&arg0(&args)) {
2109            Some(JsObj::Symbol { desc, .. }) => {
2110                desc.clone().map(|d| h.new_str(d)).unwrap_or(Value::Undef)
2111            }
2112            _ => Value::Undef,
2113        })),
2114        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
2115        "Reflect.ownKeys" => object_keys(args, 0),
2116        "Reflect.has" => {
2117            let obj = arg0(&args);
2118            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
2119            Ok(Value::Bool(has_property(&obj, &k)))
2120        }
2121        "Reflect.get" => {
2122            let obj = arg0(&args);
2123            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
2124            get_property(&obj, &k)
2125        }
2126        "Reflect.set" => {
2127            let obj = arg0(&args);
2128            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
2129            let v = args.get(2).cloned().unwrap_or(Value::Undef);
2130            set_property(&obj, &k, v);
2131            Ok(Value::Bool(true))
2132        }
2133        "JSON.stringify" => json_stringify(args),
2134        "JSON.parse" => json_parse(args),
2135        "structuredClone" => Ok(deep_clone(&arg0(&args))),
2136        "queueMicrotask" | "process.nextTick" => {
2137            let cb = arg0(&args);
2138            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2139            enqueue_microtask(name == "process.nextTick", cb, rest);
2140            Ok(Value::Undef)
2141        }
2142        "setTimeout" | "setInterval" | "setImmediate" => Ok(schedule_timer(name, args)),
2143        "clearTimeout" | "clearInterval" => {
2144            clear_timer(&arg0(&args));
2145            Ok(Value::Undef)
2146        }
2147        "Promise.resolve" => promise_resolve(arg0(&args)),
2148        "Promise.reject" => promise_reject(arg0(&args)),
2149        "Promise.all" => promise_all(args, AllMode::All),
2150        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
2151        "Promise.race" => promise_race(args, false),
2152        "Promise.any" => promise_race(args, true),
2153        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
2154        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
2155        "Promise.withResolvers" => promise_with_resolvers(),
2156        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
2157        // `cb(item, i)` result (SameValueZero), each value an array of members.
2158        "Map.groupBy" => map_group_by(args),
2159        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
2160        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
2161        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
2162        _ if name.starts_with("@@presolve:") => {
2163            let id: u32 = name[11..].parse().unwrap_or(0);
2164            host::resolve_promise_val(id, arg0(&args));
2165            Ok(Value::Undef)
2166        }
2167        _ if name.starts_with("@@preject:") => {
2168            let id: u32 = name[10..].parse().unwrap_or(0);
2169            host::reject_promise_val(id, arg0(&args));
2170            Ok(Value::Undef)
2171        }
2172        _ if name.starts_with("@@finpass:") => {
2173            // finally(cb) on fulfill: run cb, then pass the value through.
2174            let i: u32 = name[10..].parse().unwrap_or(0);
2175            let cb = Value::Obj(i);
2176            host::invoke(&cb, Vec::new(), None)?;
2177            Ok(arg0(&args))
2178        }
2179        _ if name.starts_with("@@finthrow:") => {
2180            // finally(cb) on reject: run cb, then re-throw the reason.
2181            let i: u32 = name[11..].parse().unwrap_or(0);
2182            let cb = Value::Obj(i);
2183            host::invoke(&cb, Vec::new(), None)?;
2184            let reason = arg0(&args);
2185            with_host(|h| h.exc = Some(reason.clone()));
2186            Err(with_host(|h| error_string(h, &reason)))
2187        }
2188        _ => Err(host::type_error(&format!("{name} is not a function"))),
2189    }
2190}
2191
2192/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
2193/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
2194/// (matching Node's messages).
2195fn bigint_ctor(v: &Value) -> Result<Value, String> {
2196    use num_bigint::BigInt;
2197    let big = match v {
2198        Value::Bool(b) => BigInt::from(*b as i64),
2199        Value::Int(n) => BigInt::from(*n),
2200        Value::Float(f) => {
2201            if !f.is_finite() || f.fract() != 0.0 {
2202                let disp = with_host(|h| h.str_of(v));
2203                return Err(format!(
2204                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
2205                ));
2206            }
2207            // Exact for the integer f64 range; larger integers round-trip via the
2208            // decimal string.
2209            match BigInt::parse_bytes(host::fmt_number(*f).as_bytes(), 10) {
2210                Some(b) => b,
2211                None => return Err(host::type_error("Cannot convert value to a BigInt")),
2212            }
2213        }
2214        Value::Str(s) => match host::parse_bigint_str(s) {
2215            Some(b) => b,
2216            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
2217        },
2218        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
2219            Some(JsObj::BigInt(b)) => b,
2220            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
2221                Some(b) => b,
2222                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
2223            },
2224            _ => return Err(host::type_error("Cannot convert value to a BigInt")),
2225        },
2226        _ => return Err(host::type_error("Cannot convert value to a BigInt")),
2227    };
2228    Ok(with_host(|h| h.new_bigint(big)))
2229}
2230
2231/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
2232/// its source (and flags, unless new ones are given).
2233fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
2234    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
2235        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
2236        _ => {
2237            let a0 = arg0(args);
2238            let src = if matches!(a0, Value::Undef) {
2239                String::new()
2240            } else {
2241                with_host(|h| h.str_of(&a0))
2242            };
2243            (src, None)
2244        }
2245    };
2246    let flags = match args.get(1) {
2247        Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
2248        _ => existing_flags.unwrap_or_default(),
2249    };
2250    // An empty source compiles as the JS canonical `(?:)`.
2251    let src = if source.is_empty() {
2252        "(?:)".to_string()
2253    } else {
2254        source
2255    };
2256    crate::regexp::build_regexp(&src, &flags)
2257}
2258
2259/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
2260/// two's-complement (signed) or unsigned integer.
2261fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
2262    use num_bigint::BigInt;
2263    use num_traits::Signed;
2264    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
2265    if bits < 0 {
2266        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
2267    }
2268    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
2269        Some(b) => b,
2270        None => return Err(host::type_error("Cannot convert to a BigInt")),
2271    };
2272    let bits = bits as u32;
2273    if bits == 0 {
2274        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
2275    }
2276    let modulus = BigInt::from(1) << bits; // 2^bits
2277                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
2278    let mut r = &x % &modulus;
2279    if r.is_negative() {
2280        r += &modulus;
2281    }
2282    if !unsigned {
2283        let half = BigInt::from(1) << (bits - 1);
2284        if r >= half {
2285            r -= &modulus;
2286        }
2287    }
2288    Ok(with_host(|h| h.new_bigint(r)))
2289}
2290
2291/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
2292/// interleaved with the substitutions.
2293fn string_raw(args: &[Value]) -> Result<Value, String> {
2294    let call_site = arg0(args);
2295    let raw = get_property(&call_site, "raw")?;
2296    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
2297    let mut out = String::new();
2298    for (i, r) in raws.iter().enumerate() {
2299        out.push_str(&with_host(|h| h.str_of(r)));
2300        if i + 1 < raws.len() {
2301            if let Some(sub) = args.get(i + 1) {
2302                out.push_str(&with_host(|h| h.str_of(sub)));
2303            }
2304        }
2305    }
2306    Ok(with_host(|h| h.new_str(out)))
2307}
2308
2309/// `Object(x)`: box/pass-through — for our model, non-object args just return a
2310/// fresh object; objects pass through.
2311fn object_call(args: Vec<Value>) -> Value {
2312    let a = arg0(&args);
2313    if matches!(
2314        with_host(|h| h.get(&a).cloned()),
2315        Some(JsObj::Object(_)) | Some(JsObj::Array(_))
2316    ) {
2317        a
2318    } else {
2319        with_host(|h| h.new_object(IndexMap::new()))
2320    }
2321}
2322
2323/// Construct via `new` for the builtin constructors.
2324pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
2325    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
2326    if let Some(r) = crate::stdlib::construct(name, &args) {
2327        return r;
2328    }
2329    match name {
2330        "Array" => {
2331            // new Array(n) -> length-n array; new Array(a, b) -> [a, b].
2332            if args.len() == 1 {
2333                if let Value::Float(f) = args[0] {
2334                    if f.fract() == 0.0 && f >= 0.0 {
2335                        return Ok(with_host(|h| h.new_array(vec![Value::Undef; f as usize])));
2336                    }
2337                }
2338            }
2339            Ok(with_host(|h| h.new_array(args)))
2340        }
2341        "Object" => Ok(object_call(args)),
2342        "Map" | "WeakMap" => {
2343            let weak = name == "WeakMap";
2344            let m = with_host(|h| {
2345                h.alloc(JsObj::Map {
2346                    entries: indexmap::IndexMap::new(),
2347                    weak,
2348                })
2349            });
2350            if let Some(init) = args
2351                .first()
2352                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
2353            {
2354                let pairs = host::iter_all(init)?;
2355                for p in pairs {
2356                    let kv = host::iter_all(&p)?;
2357                    let k = kv.first().cloned().unwrap_or(Value::Undef);
2358                    let v = kv.get(1).cloned().unwrap_or(Value::Undef);
2359                    map_method(&m, "set", vec![k, v])?;
2360                }
2361            }
2362            Ok(m)
2363        }
2364        "Set" | "WeakSet" => {
2365            let weak = name == "WeakSet";
2366            let s = with_host(|h| {
2367                h.alloc(JsObj::Set {
2368                    entries: indexmap::IndexMap::new(),
2369                    weak,
2370                })
2371            });
2372            if let Some(init) = args
2373                .first()
2374                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
2375            {
2376                let vals = host::iter_all(init)?;
2377                for v in vals {
2378                    set_method(&s, "add", vec![v])?;
2379                }
2380            }
2381            Ok(s)
2382        }
2383        "Promise" => new_promise(arg0(&args)),
2384        "RegExp" => regexp_ctor(&args),
2385        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
2386        "Error" => Ok(make_error(name, &args)),
2387        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
2388        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
2389    }
2390}
2391
2392fn make_error(name: &str, args: &[Value]) -> Value {
2393    with_host(|h| {
2394        h.ensure_error_protos();
2395        let mut props: IndexMap<String, Value> = IndexMap::new();
2396        let msg = args
2397            .first()
2398            .filter(|a| !matches!(a, Value::Undef))
2399            .map(|a| h.str_of(a));
2400        if let Some(m) = &msg {
2401            let mv = h.new_str(m.clone());
2402            props.insert("message".into(), mv);
2403        }
2404        // `.stack` is engine-specific; a simple `Name: message` header line
2405        // suffices for parity (the fuzzer never prints raw stacks).
2406        let stack = match &msg {
2407            Some(m) if !m.is_empty() => format!("{name}: {m}\n    at <anonymous>"),
2408            _ => format!("{name}\n    at <anonymous>"),
2409        };
2410        let sv = h.new_str(stack);
2411        props.insert("stack".into(), sv);
2412        let e = h.new_object(props);
2413        if let Some(p) = host::error_proto_of(h, name) {
2414            h.set_proto(&e, p);
2415        }
2416        e
2417    })
2418}
2419
2420fn print_line(args: &[Value], stderr: bool) {
2421    // Node's console.log(...args) === util.format(...args): printf-style
2422    // substitution when the first arg is a format string, else inspect-and-join.
2423    let line: String = crate::stdlib::util::format(args);
2424    if stderr {
2425        eprintln!("{line}");
2426    } else {
2427        println!("{line}");
2428    }
2429}
2430
2431fn arg0(args: &[Value]) -> Value {
2432    args.first().cloned().unwrap_or(Value::Undef)
2433}
2434fn arg_num(args: &[Value], i: usize) -> f64 {
2435    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
2436}
2437
2438fn is_integer(v: Value) -> bool {
2439    match v {
2440        Value::Int(_) => true,
2441        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
2442        _ => false,
2443    }
2444}
2445fn is_safe_integer(v: Value) -> bool {
2446    match v {
2447        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
2448        Value::Int(_) => true,
2449        _ => false,
2450    }
2451}
2452
2453/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
2454/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
2455/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
2456fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
2457    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
2458    const UNRESERVED: &[u8] =
2459        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
2460    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
2461    const RESERVED: &[u8] = b";,/?:@&=+$#";
2462    let mut out = String::with_capacity(s.len());
2463    for &b in s.as_bytes() {
2464        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
2465            out.push(b as char);
2466        } else {
2467            out.push('%');
2468            out.push(
2469                char::from_digit((b >> 4) as u32, 16)
2470                    .unwrap()
2471                    .to_ascii_uppercase(),
2472            );
2473            out.push(
2474                char::from_digit((b & 0xf) as u32, 16)
2475                    .unwrap()
2476                    .to_ascii_uppercase(),
2477            );
2478        }
2479    }
2480    Ok(with_host(|h| h.new_str(out)))
2481}
2482
2483/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
2484/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
2485/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
2486fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
2487    const RESERVED: &[u8] = b";,/?:@&=+$#";
2488    let bytes = s.as_bytes();
2489    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
2490    let mut i = 0;
2491    while i < bytes.len() {
2492        if bytes[i] == b'%' {
2493            if i + 2 >= bytes.len() {
2494                return Err("URIError: URI malformed".into());
2495            }
2496            let hi = (bytes[i + 1] as char).to_digit(16);
2497            let lo = (bytes[i + 2] as char).to_digit(16);
2498            match (hi, lo) {
2499                (Some(h), Some(l)) => {
2500                    let byte = (h * 16 + l) as u8;
2501                    // decodeURI keeps reserved-delimiter escapes literal.
2502                    if uri && RESERVED.contains(&byte) {
2503                        out.extend_from_slice(&bytes[i..i + 3]);
2504                    } else {
2505                        out.push(byte);
2506                    }
2507                    i += 3;
2508                }
2509                _ => return Err("URIError: URI malformed".into()),
2510            }
2511        } else {
2512            out.push(bytes[i]);
2513            i += 1;
2514        }
2515    }
2516    match String::from_utf8(out) {
2517        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
2518        Err(_) => Err("URIError: URI malformed".into()),
2519    }
2520}
2521
2522fn parse_int(args: &[Value]) -> f64 {
2523    let s = with_host(|h| h.str_of(&arg0(args)));
2524    let radix = args
2525        .get(1)
2526        .map(|r| with_host(|h| h.to_number(r)) as u32)
2527        .filter(|r| (2..=36).contains(r));
2528    let t = s.trim();
2529    let (neg, digits) = match t.strip_prefix('-') {
2530        Some(rest) => (true, rest),
2531        None => (false, t.strip_prefix('+').unwrap_or(t)),
2532    };
2533    let (radix, digits) = match radix {
2534        Some(16) => (
2535            16u32,
2536            digits
2537                .strip_prefix("0x")
2538                .or_else(|| digits.strip_prefix("0X"))
2539                .unwrap_or(digits),
2540        ),
2541        Some(r) => (r, digits),
2542        None => {
2543            if let Some(hex) = digits
2544                .strip_prefix("0x")
2545                .or_else(|| digits.strip_prefix("0X"))
2546            {
2547                (16, hex)
2548            } else {
2549                (10, digits)
2550            }
2551        }
2552    };
2553    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
2554    if valid.is_empty() {
2555        return f64::NAN;
2556    }
2557    let n = i64::from_str_radix(&valid, radix)
2558        .map(|n| n as f64)
2559        .unwrap_or(f64::NAN);
2560    if neg {
2561        -n
2562    } else {
2563        n
2564    }
2565}
2566
2567fn parse_float(args: &[Value]) -> f64 {
2568    let s = with_host(|h| h.str_of(&arg0(args)));
2569    let t = s.trim_start();
2570    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
2571    let inf_body = t
2572        .strip_prefix('+')
2573        .or_else(|| t.strip_prefix('-'))
2574        .unwrap_or(t);
2575    if inf_body.starts_with("Infinity") {
2576        return if t.starts_with('-') {
2577            f64::NEG_INFINITY
2578        } else {
2579            f64::INFINITY
2580        };
2581    }
2582    // Longest numeric prefix.
2583    let mut end = 0;
2584    let bytes = t.as_bytes();
2585    let mut seen_dot = false;
2586    let mut seen_e = false;
2587    for (i, &c) in bytes.iter().enumerate() {
2588        match c {
2589            b'0'..=b'9' => end = i + 1,
2590            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => end = i + 1,
2591            b'.' if !seen_dot && !seen_e => {
2592                seen_dot = true;
2593                end = i + 1;
2594            }
2595            b'e' | b'E' if !seen_e && i > 0 => {
2596                seen_e = true;
2597                end = i + 1;
2598            }
2599            _ => break,
2600        }
2601    }
2602    t[..end].parse::<f64>().unwrap_or(f64::NAN)
2603}
2604
2605fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
2606    let x = arg_num(args, 0);
2607    let r = match fname {
2608        "floor" => x.floor(),
2609        "ceil" => x.ceil(),
2610        "round" => {
2611            // JS rounds half up toward +Infinity, but preserves the sign of a
2612            // zero result: Math.round(-0.5) === -0, Math.round(-0.4) === -0.
2613            let r = (x + 0.5).floor();
2614            if r == 0.0 && x.is_sign_negative() {
2615                -0.0
2616            } else {
2617                r
2618            }
2619        }
2620        "trunc" => x.trunc(),
2621        "abs" => x.abs(),
2622        "sign" => {
2623            if x.is_nan() {
2624                f64::NAN
2625            } else if x > 0.0 {
2626                1.0
2627            } else if x < 0.0 {
2628                -1.0
2629            } else {
2630                x
2631            }
2632        }
2633        "sqrt" => x.sqrt(),
2634        "cbrt" => x.cbrt(),
2635        "exp" => x.exp(),
2636        "log" => x.ln(),
2637        "log2" => x.log2(),
2638        "log10" => x.log10(),
2639        "sin" => x.sin(),
2640        "cos" => x.cos(),
2641        "tan" => x.tan(),
2642        "asin" => x.asin(),
2643        "acos" => x.acos(),
2644        "atan" => x.atan(),
2645        "atan2" => x.atan2(arg_num(args, 1)),
2646        "pow" => x.powf(arg_num(args, 1)),
2647        "hypot" => {
2648            // Scale by the largest magnitude before squaring — this avoids the
2649            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
2650            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
2651            let mut max = 0.0f64;
2652            for x in &xs {
2653                if x.abs() > max {
2654                    max = x.abs();
2655                }
2656            }
2657            if xs.iter().any(|x| x.is_infinite()) {
2658                f64::INFINITY
2659            } else if max == 0.0 || !max.is_finite() {
2660                max
2661            } else {
2662                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
2663                max * s.sqrt()
2664            }
2665        }
2666        "random" => pseudo_random(),
2667        "max" => {
2668            if args.is_empty() {
2669                f64::NEG_INFINITY
2670            } else {
2671                let mut m = f64::NEG_INFINITY;
2672                for a in args {
2673                    let n = with_host(|h| h.to_number(a));
2674                    if n.is_nan() {
2675                        return Ok(Value::Float(f64::NAN));
2676                    }
2677                    if n > m {
2678                        m = n;
2679                    }
2680                }
2681                m
2682            }
2683        }
2684        "min" => {
2685            if args.is_empty() {
2686                f64::INFINITY
2687            } else {
2688                let mut m = f64::INFINITY;
2689                for a in args {
2690                    let n = with_host(|h| h.to_number(a));
2691                    if n.is_nan() {
2692                        return Ok(Value::Float(f64::NAN));
2693                    }
2694                    if n < m {
2695                        m = n;
2696                    }
2697                }
2698                m
2699            }
2700        }
2701        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
2702        "clz32" => {
2703            let u = if x.is_finite() {
2704                x.trunc().rem_euclid(4294967296.0) as u32
2705            } else {
2706                0
2707            };
2708            u.leading_zeros() as f64
2709        }
2710        // Round to the nearest single-precision float.
2711        "fround" => (x as f32) as f64,
2712        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
2713    };
2714    Ok(Value::Float(r))
2715}
2716
2717/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
2718/// Node by nature; kept simple).
2719fn pseudo_random() -> f64 {
2720    use std::cell::Cell;
2721    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
2722    SEED.with(|s| {
2723        let mut x = s.get();
2724        x ^= x << 13;
2725        x ^= x >> 7;
2726        x ^= x << 17;
2727        s.set(x);
2728        (x >> 11) as f64 / (1u64 << 53) as f64
2729    })
2730}
2731
2732// ── Object.* ──────────────────────────────────────────────────────────────────
2733
2734fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
2735    let v = arg0(&args);
2736    // A builtin prototype namespace that exposes enumerable methods for copying
2737    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
2738    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&v).cloned()) {
2739        if let Some(names) = builtin_proto_method_names(&ns) {
2740            return Ok(with_host(|h| {
2741                let out: Vec<Value> = names
2742                    .iter()
2743                    .map(|name| match mode {
2744                        1 => h.alloc(JsObj::Builtin(format!(
2745                            "@proto:{}:{name}",
2746                            ns.trim_end_matches(".prototype")
2747                        ))),
2748                        2 => {
2749                            let ks = h.new_str(*name);
2750                            let val = h.alloc(JsObj::Builtin(format!(
2751                                "@proto:{}:{name}",
2752                                ns.trim_end_matches(".prototype")
2753                            )));
2754                            h.new_array(vec![ks, val])
2755                        }
2756                        _ => h.new_str(*name),
2757                    })
2758                    .collect();
2759                h.new_array(out)
2760            }));
2761        }
2762    }
2763    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&v) {
2764        Some(JsObj::Object(props)) => props
2765            .iter()
2766            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
2767            .map(|(k, val)| (k.clone(), val.clone()))
2768            .collect(),
2769        Some(JsObj::Array(items)) => items
2770            .iter()
2771            .enumerate()
2772            .map(|(i, val)| (i.to_string(), val.clone()))
2773            .collect(),
2774        _ => Vec::new(),
2775    });
2776    Ok(with_host(|h| {
2777        let out: Vec<Value> = entries
2778            .into_iter()
2779            .map(|(k, val)| match mode {
2780                0 => h.new_str(k),
2781                1 => val,
2782                _ => {
2783                    let ks = h.new_str(k);
2784                    h.new_array(vec![ks, val])
2785                }
2786            })
2787            .collect();
2788        h.new_array(out)
2789    }))
2790}
2791
2792fn object_assign(args: Vec<Value>) -> Result<Value, String> {
2793    let target = arg0(&args);
2794    for src in args.iter().skip(1) {
2795        let entries: Vec<(String, Value)> = with_host(|h| match h.get(src) {
2796            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
2797            _ => Vec::new(),
2798        });
2799        with_host(|h| {
2800            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
2801                for (k, v) in entries {
2802                    p.insert(k, v);
2803                }
2804                host::canonicalize_own_keys(p);
2805            }
2806        });
2807    }
2808    Ok(target)
2809}
2810
2811fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
2812    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
2813    let mut props: IndexMap<String, Value> = IndexMap::new();
2814    for p in pairs {
2815        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
2816        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
2817        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
2818        props.insert(key, val);
2819    }
2820    Ok(with_host(|h| h.new_object(props)))
2821}
2822
2823/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
2824/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
2825/// members mapped to that key, in first-seen key order.
2826fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
2827    let items = host::iter_all(&arg0(&args))?;
2828    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
2829    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
2830    for (i, item) in items.into_iter().enumerate() {
2831        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
2832        let key = with_host(|h| h.property_key(&key_v));
2833        groups.entry(key).or_default().push(item);
2834    }
2835    let props: IndexMap<String, Value> = with_host(|h| {
2836        groups
2837            .into_iter()
2838            .map(|(k, v)| (k, h.new_array(v)))
2839            .collect()
2840    });
2841    let obj = with_host(|h| h.new_object(props));
2842    // A null-prototype object (as Node returns), so it has no inherited members.
2843    with_host(|h| {
2844        let nv = h.null();
2845        h.set_proto(&obj, nv);
2846    });
2847    Ok(obj)
2848}
2849
2850/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
2851/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
2852fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
2853    let items = host::iter_all(&arg0(&args))?;
2854    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
2855    let m = with_host(|h| {
2856        h.alloc(JsObj::Map {
2857            entries: IndexMap::new(),
2858            weak: false,
2859        })
2860    });
2861    for (i, item) in items.into_iter().enumerate() {
2862        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
2863        let existing = map_method(&m, "get", vec![key_v.clone()])?;
2864        if matches!(existing, Value::Undef) {
2865            let arr = with_host(|h| h.new_array(vec![item]));
2866            map_method(&m, "set", vec![key_v, arr])?;
2867        } else {
2868            with_host(|h| {
2869                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
2870                    a.push(item);
2871                }
2872            });
2873        }
2874    }
2875    Ok(m)
2876}
2877
2878fn array_from(args: Vec<Value>) -> Result<Value, String> {
2879    // `Array.from` accepts generators and user iterables, plus array-likes with a
2880    // numeric `.length`.
2881    let src = arg0(&args);
2882    let items = match host::iter_all(&src) {
2883        Ok(v) => v,
2884        Err(_) => array_like_items(&src),
2885    };
2886    if let Some(cb) = args.get(1).cloned() {
2887        let mut out = Vec::with_capacity(items.len());
2888        for (i, it) in items.into_iter().enumerate() {
2889            out.push(host::invoke(&cb, vec![it, Value::Float(i as f64)], None)?);
2890        }
2891        return Ok(with_host(|h| h.new_array(out)));
2892    }
2893    Ok(with_host(|h| h.new_array(items)))
2894}
2895
2896/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
2897fn array_like_items(src: &Value) -> Vec<Value> {
2898    let len = get_property(src, "length")
2899        .ok()
2900        .map(|l| with_host(|h| h.to_number(&l)))
2901        .unwrap_or(0.0);
2902    if !len.is_finite() || len <= 0.0 {
2903        return Vec::new();
2904    }
2905    (0..len as usize)
2906        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
2907        .collect()
2908}
2909
2910// ── JSON ──────────────────────────────────────────────────────────────────────
2911
2912fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
2913    let v = arg0(&args);
2914    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
2915    // bigint form), matching Node's exact message.
2916    if with_host(|h| json_has_bigint(h, &v)) {
2917        return Err(host::type_error("Do not know how to serialize a BigInt"));
2918    }
2919    let indent = match args.get(2) {
2920        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
2921        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
2922        None => String::new(),
2923    };
2924    // A replacer array (args[1]) restricts which object keys are serialized.
2925    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
2926        with_host(|h| match h.get(r) {
2927            Some(JsObj::Array(items)) => {
2928                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
2929            }
2930            _ => None,
2931        })
2932    });
2933    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
2934    match s {
2935        Some(s) => Ok(with_host(|h| h.new_str(s))),
2936        None => Ok(Value::Undef),
2937    }
2938}
2939
2940/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
2941/// try to serialize (a value in an array/object) — such a value throws.
2942fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
2943    match h.get(v) {
2944        Some(JsObj::BigInt(_)) => true,
2945        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
2946        Some(JsObj::Object(props)) => props
2947            .iter()
2948            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
2949            .any(|(_, val)| json_has_bigint(h, val)),
2950        _ => false,
2951    }
2952}
2953
2954fn json_str(
2955    h: &host::JsHost,
2956    v: &Value,
2957    indent: &str,
2958    depth: usize,
2959    keys: Option<&[String]>,
2960) -> Option<String> {
2961    let sep = if indent.is_empty() { ":" } else { ": " };
2962    match v {
2963        Value::Undef => None,
2964        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
2965        Value::Int(n) => Some(n.to_string()),
2966        Value::Float(f) => Some(if f.is_finite() {
2967            host::fmt_number(*f)
2968        } else {
2969            "null".into()
2970        }),
2971        Value::Str(s) => Some(json_quote(s)),
2972        Value::Obj(_) => match h.get(v) {
2973            Some(JsObj::Str(s)) => Some(json_quote(s)),
2974            Some(JsObj::Null) => Some("null".into()),
2975            // Map/Set have no enumerable own string keys → serialize as `{}`.
2976            Some(JsObj::Map { .. }) | Some(JsObj::Set { .. }) => Some("{}".into()),
2977            // Functions and symbols are omitted (undefined) as values.
2978            Some(JsObj::Func(_))
2979            | Some(JsObj::Builtin(_))
2980            | Some(JsObj::BoundMethod { .. })
2981            | Some(JsObj::BoundFunc { .. })
2982            | Some(JsObj::Class(_))
2983            | Some(JsObj::Symbol { .. })
2984            | Some(JsObj::Generator { .. }) => None,
2985            Some(JsObj::Array(items)) => {
2986                if items.is_empty() {
2987                    return Some("[]".into());
2988                }
2989                let parts: Vec<String> = items
2990                    .iter()
2991                    .map(|x| {
2992                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
2993                    })
2994                    .collect();
2995                Some(wrap(&parts, "[", "]", indent, depth))
2996            }
2997            Some(JsObj::Object(props)) => {
2998                // A replacer array restricts (and orders) which keys are emitted.
2999                let parts: Vec<String> = match keys {
3000                    Some(allow) => allow
3001                        .iter()
3002                        .filter_map(|k| {
3003                            props.get(k).and_then(|val| {
3004                                json_str(h, val, indent, depth + 1, keys)
3005                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
3006                            })
3007                        })
3008                        .collect(),
3009                    None => props
3010                        .iter()
3011                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
3012                        .filter_map(|(k, val)| {
3013                            json_str(h, val, indent, depth + 1, keys)
3014                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
3015                        })
3016                        .collect(),
3017                };
3018                if parts.is_empty() {
3019                    return Some("{}".into());
3020                }
3021                Some(wrap(&parts, "{", "}", indent, depth))
3022            }
3023            _ => Some("null".into()),
3024        },
3025        _ => Some("null".into()),
3026    }
3027}
3028
3029fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
3030    if indent.is_empty() {
3031        format!("{open}{}{close}", parts.join(","))
3032    } else {
3033        let pad = indent.repeat(depth + 1);
3034        let pad_close = indent.repeat(depth);
3035        format!(
3036            "{open}\n{pad}{}\n{pad_close}{close}",
3037            parts.join(&format!(",\n{pad}"))
3038        )
3039    }
3040}
3041
3042fn json_quote(s: &str) -> String {
3043    let mut out = String::from("\"");
3044    for c in s.chars() {
3045        match c {
3046            '"' => out.push_str("\\\""),
3047            '\\' => out.push_str("\\\\"),
3048            '\n' => out.push_str("\\n"),
3049            '\t' => out.push_str("\\t"),
3050            '\r' => out.push_str("\\r"),
3051            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
3052            _ => out.push(c),
3053        }
3054    }
3055    out.push('"');
3056    out
3057}
3058
3059fn json_parse(args: Vec<Value>) -> Result<Value, String> {
3060    let s = with_host(|h| h.str_of(&arg0(&args)));
3061    let mut p = JsonParser {
3062        chars: s.chars().collect(),
3063        pos: 0,
3064    };
3065    p.skip_ws();
3066    let v = p.parse_value()?;
3067    p.skip_ws();
3068    // Optional reviver: walk bottom-up, transforming each (key, value).
3069    if let Some(reviver) = args
3070        .get(1)
3071        .filter(|r| with_host(|h| host::is_callable(h, r)))
3072        .cloned()
3073    {
3074        return json_revive("", v, &reviver);
3075    }
3076    Ok(v)
3077}
3078
3079/// `JSON.parse` reviver walk: recurse into children first, then call
3080/// `reviver(key, value)`; a returned `undefined` drops the property.
3081fn json_revive(key: &str, val: Value, reviver: &Value) -> Result<Value, String> {
3082    match with_host(|h| h.get(&val).cloned()) {
3083        Some(JsObj::Array(items)) => {
3084            for i in 0..items.len() {
3085                let elem = with_host(|h| match h.get(&val) {
3086                    Some(JsObj::Array(it)) => it[i].clone(),
3087                    _ => Value::Undef,
3088                });
3089                let nv = json_revive(&i.to_string(), elem, reviver)?;
3090                with_host(|h| {
3091                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
3092                        it[i] = nv;
3093                    }
3094                });
3095            }
3096        }
3097        Some(JsObj::Object(props)) => {
3098            let keys: Vec<String> = props
3099                .keys()
3100                .filter(|k| !k.starts_with("@@"))
3101                .cloned()
3102                .collect();
3103            for k in keys {
3104                let elem = with_host(|h| match h.get(&val) {
3105                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
3106                    _ => Value::Undef,
3107                });
3108                let nv = json_revive(&k, elem, reviver)?;
3109                with_host(|h| {
3110                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
3111                        if matches!(nv, Value::Undef) {
3112                            p.shift_remove(&k);
3113                        } else {
3114                            p.insert(k.clone(), nv);
3115                        }
3116                    }
3117                });
3118            }
3119        }
3120        _ => {}
3121    }
3122    let kv = with_host(|h| h.new_str(key.to_string()));
3123    host::invoke(reviver, vec![kv, val], None)
3124}
3125
3126struct JsonParser {
3127    chars: Vec<char>,
3128    pos: usize,
3129}
3130impl JsonParser {
3131    fn peek(&self) -> Option<char> {
3132        self.chars.get(self.pos).copied()
3133    }
3134    fn skip_ws(&mut self) {
3135        while matches!(
3136            self.peek(),
3137            Some(' ') | Some('\n') | Some('\t') | Some('\r')
3138        ) {
3139            self.pos += 1;
3140        }
3141    }
3142    fn parse_value(&mut self) -> Result<Value, String> {
3143        self.skip_ws();
3144        match self.peek() {
3145            Some('{') => self.parse_object(),
3146            Some('[') => self.parse_array(),
3147            Some('"') => {
3148                let s = self.parse_string()?;
3149                Ok(with_host(|h| h.new_str(s)))
3150            }
3151            Some('t') | Some('f') => self.parse_bool(),
3152            Some('n') => {
3153                self.expect_lit("null")?;
3154                Ok(with_host(|h| h.null()))
3155            }
3156            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
3157            _ => Err("SyntaxError: Unexpected token in JSON".into()),
3158        }
3159    }
3160    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
3161        for ch in lit.chars() {
3162            if self.peek() != Some(ch) {
3163                return Err("SyntaxError: Unexpected token in JSON".into());
3164            }
3165            self.pos += 1;
3166        }
3167        Ok(())
3168    }
3169    fn parse_bool(&mut self) -> Result<Value, String> {
3170        if self.peek() == Some('t') {
3171            self.expect_lit("true")?;
3172            Ok(Value::Bool(true))
3173        } else {
3174            self.expect_lit("false")?;
3175            Ok(Value::Bool(false))
3176        }
3177    }
3178    fn parse_number(&mut self) -> Result<Value, String> {
3179        let start = self.pos;
3180        while matches!(self.peek(), Some(c) if c.is_ascii_digit() || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E')
3181        {
3182            self.pos += 1;
3183        }
3184        let s: String = self.chars[start..self.pos].iter().collect();
3185        s.parse::<f64>()
3186            .map(Value::Float)
3187            .map_err(|_| "SyntaxError: bad number in JSON".into())
3188    }
3189    fn parse_string(&mut self) -> Result<String, String> {
3190        self.pos += 1; // opening quote
3191        let mut out = String::new();
3192        loop {
3193            match self.peek() {
3194                None => return Err("SyntaxError: unterminated string in JSON".into()),
3195                Some('"') => {
3196                    self.pos += 1;
3197                    break;
3198                }
3199                Some('\\') => {
3200                    self.pos += 1;
3201                    match self.peek() {
3202                        Some('n') => out.push('\n'),
3203                        Some('t') => out.push('\t'),
3204                        Some('r') => out.push('\r'),
3205                        Some('"') => out.push('"'),
3206                        Some('\\') => out.push('\\'),
3207                        Some('/') => out.push('/'),
3208                        Some('b') => out.push('\u{08}'),
3209                        Some('f') => out.push('\u{0C}'),
3210                        Some('u') => {
3211                            let h: String = self.chars
3212                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
3213                                .iter()
3214                                .collect();
3215                            if let Ok(n) = u32::from_str_radix(&h, 16) {
3216                                if let Some(ch) = char::from_u32(n) {
3217                                    out.push(ch);
3218                                }
3219                            }
3220                            self.pos += 4;
3221                        }
3222                        _ => {}
3223                    }
3224                    self.pos += 1;
3225                }
3226                Some(c) => {
3227                    out.push(c);
3228                    self.pos += 1;
3229                }
3230            }
3231        }
3232        Ok(out)
3233    }
3234    fn parse_array(&mut self) -> Result<Value, String> {
3235        self.pos += 1; // [
3236        let mut items = Vec::new();
3237        self.skip_ws();
3238        if self.peek() == Some(']') {
3239            self.pos += 1;
3240            return Ok(with_host(|h| h.new_array(items)));
3241        }
3242        loop {
3243            items.push(self.parse_value()?);
3244            self.skip_ws();
3245            match self.peek() {
3246                Some(',') => {
3247                    self.pos += 1;
3248                }
3249                Some(']') => {
3250                    self.pos += 1;
3251                    break;
3252                }
3253                _ => return Err("SyntaxError: bad array in JSON".into()),
3254            }
3255        }
3256        Ok(with_host(|h| h.new_array(items)))
3257    }
3258    fn parse_object(&mut self) -> Result<Value, String> {
3259        self.pos += 1; // {
3260        let mut props: IndexMap<String, Value> = IndexMap::new();
3261        self.skip_ws();
3262        if self.peek() == Some('}') {
3263            self.pos += 1;
3264            return Ok(with_host(|h| h.new_object(props)));
3265        }
3266        loop {
3267            self.skip_ws();
3268            let key = self.parse_string()?;
3269            self.skip_ws();
3270            if self.peek() != Some(':') {
3271                return Err("SyntaxError: expected ':' in JSON".into());
3272            }
3273            self.pos += 1;
3274            let val = self.parse_value()?;
3275            props.insert(key, val);
3276            self.skip_ws();
3277            match self.peek() {
3278                Some(',') => {
3279                    self.pos += 1;
3280                }
3281                Some('}') => {
3282                    self.pos += 1;
3283                    break;
3284                }
3285                _ => return Err("SyntaxError: bad object in JSON".into()),
3286            }
3287        }
3288        Ok(with_host(|h| h.new_object(props)))
3289    }
3290}
3291
3292// ══ type methods (array / string / number) ═══════════════════════════════════
3293
3294fn is_array_method(name: &str) -> bool {
3295    matches!(
3296        name,
3297        "push"
3298            | "pop"
3299            | "shift"
3300            | "unshift"
3301            | "map"
3302            | "filter"
3303            | "forEach"
3304            | "join"
3305            | "slice"
3306            | "indexOf"
3307            | "lastIndexOf"
3308            | "includes"
3309            | "reduce"
3310            | "concat"
3311            | "reverse"
3312            | "sort"
3313            | "find"
3314            | "findIndex"
3315            | "some"
3316            | "every"
3317            | "flat"
3318            | "fill"
3319            | "splice"
3320            | "keys"
3321            | "values"
3322            | "entries"
3323            | "flatMap"
3324            | "at"
3325            | "toString"
3326            | "reduceRight"
3327            | "findLast"
3328            | "findLastIndex"
3329            | "copyWithin"
3330    )
3331}
3332fn is_string_method(name: &str) -> bool {
3333    matches!(
3334        name,
3335        "toUpperCase"
3336            | "toLowerCase"
3337            | "charAt"
3338            | "charCodeAt"
3339            | "codePointAt"
3340            | "indexOf"
3341            | "lastIndexOf"
3342            | "includes"
3343            | "slice"
3344            | "substring"
3345            | "substr"
3346            | "split"
3347            | "trim"
3348            | "trimStart"
3349            | "trimEnd"
3350            | "replace"
3351            | "replaceAll"
3352            | "repeat"
3353            | "startsWith"
3354            | "endsWith"
3355            | "padStart"
3356            | "padEnd"
3357            | "concat"
3358            | "at"
3359            | "toString"
3360            | "valueOf"
3361            | "match"
3362            | "matchAll"
3363            | "search"
3364            | "normalize"
3365            | "localeCompare"
3366    )
3367}
3368
3369/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
3370fn is_regexp_arg(v: &Value) -> bool {
3371    matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::RegExp(_)))
3372}
3373
3374/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
3375/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
3376fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
3377    if pat.is_empty() {
3378        return Ok(s.to_string());
3379    }
3380    let mut out = String::new();
3381    let mut rest = s;
3382    let mut base = 0usize;
3383    while let Some(pos) = rest.find(pat) {
3384        out.push_str(&rest[..pos]);
3385        let offset = base + pos;
3386        let m = with_host(|h| h.new_str(pat.to_string()));
3387        let str_arg = with_host(|h| h.new_str(s.to_string()));
3388        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
3389        out.push_str(&with_host(|h| h.str_of(&r)));
3390        let consumed = pos + pat.len();
3391        base += consumed;
3392        rest = &rest[consumed..];
3393        if !all {
3394            break;
3395        }
3396    }
3397    out.push_str(rest);
3398    Ok(out)
3399}
3400fn is_number_method(name: &str) -> bool {
3401    matches!(
3402        name,
3403        "toFixed" | "toString" | "toPrecision" | "toLocaleString" | "valueOf"
3404    )
3405}
3406
3407/// Dispatch `recv.name(args)` for the built-in prototype methods.
3408pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
3409    let obj = with_host(|h| h.get(recv).cloned());
3410    match obj {
3411        Some(JsObj::Array(_)) => array_method(recv, name, args),
3412        Some(JsObj::Str(s)) => string_method(&s, name, args),
3413        Some(JsObj::Map { .. }) => map_method(recv, name, args),
3414        Some(JsObj::Set { .. }) => set_method(recv, name, args),
3415        Some(JsObj::Generator { .. }) => generator_method(recv, name, args),
3416        Some(JsObj::Promise { .. }) => promise_method(recv, name, args),
3417        Some(JsObj::Iter { .. }) => iter_method(recv, name, args),
3418        Some(JsObj::Symbol { .. }) => symbol_method(recv, name, args),
3419        Some(JsObj::BigInt(b)) => bigint_method(&b, name, args),
3420        Some(JsObj::RegExp(_)) => crate::regexp::regexp_method(recv, name, args),
3421        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
3422            match function_builtin_method(recv, name, &args)? {
3423                Some(v) => Ok(v),
3424                None => Err(host::type_error(&format!("{name} is not a function"))),
3425            }
3426        }
3427        Some(JsObj::Object(props)) => {
3428            if let Some(f) = props.get(name).cloned() {
3429                host::invoke(&f, args, Some(recv.clone()))
3430            } else if name == "hasOwnProperty" {
3431                let k = with_host(|h| h.str_of(&arg0(&args)));
3432                Ok(Value::Bool(props.contains_key(&k)))
3433            } else if name == "toString" {
3434                Ok(with_host(|h| h.new_str("[object Object]")))
3435            } else {
3436                Err(host::type_error(&format!("{} is not a function", name)))
3437            }
3438        }
3439        _ => {
3440            // Primitive number/bool/string coercions.
3441            if let Value::Float(_) | Value::Int(_) = recv {
3442                return number_method(with_host(|h| h.to_number(recv)), name, args);
3443            }
3444            if let Some(s) = with_host(|h| h.as_str(recv)) {
3445                return string_method(&s, name, args);
3446            }
3447            Err(host::type_error(&format!("{} is not a function", name)))
3448        }
3449    }
3450}
3451
3452fn array_items(recv: &Value) -> Vec<Value> {
3453    with_host(|h| match h.get(recv) {
3454        Some(JsObj::Array(items)) => items.clone(),
3455        _ => Vec::new(),
3456    })
3457}
3458
3459fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
3460    match name {
3461        "push" => {
3462            with_host(|h| {
3463                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3464                    items.extend(args.iter().cloned());
3465                }
3466            });
3467            Ok(Value::Float(array_items(recv).len() as f64))
3468        }
3469        "pop" => Ok(with_host(|h| {
3470            if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3471                items.pop().unwrap_or(Value::Undef)
3472            } else {
3473                Value::Undef
3474            }
3475        })),
3476        "shift" => Ok(with_host(|h| {
3477            if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3478                if items.is_empty() {
3479                    Value::Undef
3480                } else {
3481                    items.remove(0)
3482                }
3483            } else {
3484                Value::Undef
3485            }
3486        })),
3487        "unshift" => {
3488            with_host(|h| {
3489                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3490                    for (i, a) in args.iter().enumerate() {
3491                        items.insert(i, a.clone());
3492                    }
3493                }
3494            });
3495            Ok(Value::Float(array_items(recv).len() as f64))
3496        }
3497        "join" => {
3498            let sep = if args.is_empty() {
3499                ",".to_string()
3500            } else {
3501                with_host(|h| h.str_of(&args[0]))
3502            };
3503            let items = array_items(recv);
3504            let s = with_host(|h| {
3505                items
3506                    .iter()
3507                    .map(|x| match x {
3508                        Value::Undef => String::new(),
3509                        _ if h.is_null(x) => String::new(),
3510                        _ => h.str_of(x),
3511                    })
3512                    .collect::<Vec<_>>()
3513                    .join(&sep)
3514            });
3515            Ok(with_host(|h| h.new_str(s)))
3516        }
3517        "indexOf" => {
3518            let items = array_items(recv);
3519            let target = arg0(&args);
3520            let idx = with_host(|h| items.iter().position(|x| h.strict_eq(x, &target)));
3521            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
3522        }
3523        "lastIndexOf" => {
3524            let items = array_items(recv);
3525            let target = arg0(&args);
3526            let idx = with_host(|h| items.iter().rposition(|x| h.strict_eq(x, &target)));
3527            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
3528        }
3529        "includes" => {
3530            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
3531            let items = array_items(recv);
3532            let target = arg0(&args);
3533            let tnan = matches!(target, Value::Float(f) if f.is_nan());
3534            Ok(Value::Bool(with_host(|h| {
3535                items.iter().any(|x| {
3536                    (tnan && matches!(x, Value::Float(f) if f.is_nan())) || h.strict_eq(x, &target)
3537                })
3538            })))
3539        }
3540        "slice" => {
3541            let items = array_items(recv);
3542            let (lo, hi) = slice_bounds(&args, items.len());
3543            Ok(with_host(|h| h.new_array(items[lo..hi].to_vec())))
3544        }
3545        "concat" => {
3546            let mut out = array_items(recv);
3547            for a in &args {
3548                match with_host(|h| h.get(a).cloned()) {
3549                    Some(JsObj::Array(items)) => out.extend(items),
3550                    _ => out.push(a.clone()),
3551                }
3552            }
3553            Ok(with_host(|h| h.new_array(out)))
3554        }
3555        "reverse" => {
3556            with_host(|h| {
3557                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3558                    items.reverse();
3559                }
3560            });
3561            Ok(recv.clone())
3562        }
3563        "fill" => {
3564            // fill(value[, start[, end]]) — negative indices count from the end.
3565            let val = arg0(&args);
3566            let len = array_items(recv).len() as i64;
3567            let norm =
3568                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
3569            let start = if args.len() >= 2 {
3570                norm(arg_num(&args, 1) as i64)
3571            } else {
3572                0
3573            };
3574            let end = if args.len() >= 3 {
3575                norm(arg_num(&args, 2) as i64)
3576            } else {
3577                len as usize
3578            };
3579            with_host(|h| {
3580                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
3581                    for it in items.iter_mut().take(end).skip(start) {
3582                        *it = val.clone();
3583                    }
3584                }
3585            });
3586            Ok(recv.clone())
3587        }
3588        "copyWithin" => {
3589            // copyWithin(target, start[, end]) — copy a slice within the array.
3590            let items = array_items(recv);
3591            let len = items.len() as i64;
3592            let norm =
3593                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
3594            let target = norm(arg_num(&args, 0) as i64);
3595            let start = if args.len() >= 2 {
3596                norm(arg_num(&args, 1) as i64)
3597            } else {
3598                0
3599            };
3600            let end = if args.len() >= 3 {
3601                norm(arg_num(&args, 2) as i64)
3602            } else {
3603                len as usize
3604            };
3605            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
3606            with_host(|h| {
3607                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
3608                    for (k, v) in slice.into_iter().enumerate() {
3609                        if target + k < a.len() {
3610                            a[target + k] = v;
3611                        }
3612                    }
3613                }
3614            });
3615            Ok(recv.clone())
3616        }
3617        "at" => {
3618            let items = array_items(recv);
3619            let mut i = arg_num(&args, 0) as i64;
3620            if i < 0 {
3621                i += items.len() as i64;
3622            }
3623            Ok(if i >= 0 && (i as usize) < items.len() {
3624                items[i as usize].clone()
3625            } else {
3626                Value::Undef
3627            })
3628        }
3629        "map" => {
3630            let items = array_items(recv);
3631            let cb = arg0(&args);
3632            let mut out = Vec::with_capacity(items.len());
3633            for (i, it) in items.iter().enumerate() {
3634                out.push(host::invoke(
3635                    &cb,
3636                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3637                    None,
3638                )?);
3639            }
3640            Ok(with_host(|h| h.new_array(out)))
3641        }
3642        "flatMap" => {
3643            let items = array_items(recv);
3644            let cb = arg0(&args);
3645            let mut out = Vec::new();
3646            for (i, it) in items.iter().enumerate() {
3647                let r = host::invoke(
3648                    &cb,
3649                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3650                    None,
3651                )?;
3652                match with_host(|h| h.get(&r).cloned()) {
3653                    Some(JsObj::Array(inner)) => out.extend(inner),
3654                    _ => out.push(r),
3655                }
3656            }
3657            Ok(with_host(|h| h.new_array(out)))
3658        }
3659        "filter" => {
3660            let items = array_items(recv);
3661            let cb = arg0(&args);
3662            let mut out = Vec::new();
3663            for (i, it) in items.iter().enumerate() {
3664                let keep = host::invoke(
3665                    &cb,
3666                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3667                    None,
3668                )?;
3669                if with_host(|h| h.truthy(&keep)) {
3670                    out.push(it.clone());
3671                }
3672            }
3673            Ok(with_host(|h| h.new_array(out)))
3674        }
3675        "forEach" => {
3676            let items = array_items(recv);
3677            let cb = arg0(&args);
3678            for (i, it) in items.iter().enumerate() {
3679                host::invoke(
3680                    &cb,
3681                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3682                    None,
3683                )?;
3684            }
3685            Ok(Value::Undef)
3686        }
3687        "find" => {
3688            let items = array_items(recv);
3689            let cb = arg0(&args);
3690            for (i, it) in items.iter().enumerate() {
3691                let m = host::invoke(
3692                    &cb,
3693                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3694                    None,
3695                )?;
3696                if with_host(|h| h.truthy(&m)) {
3697                    return Ok(it.clone());
3698                }
3699            }
3700            Ok(Value::Undef)
3701        }
3702        "findIndex" => {
3703            let items = array_items(recv);
3704            let cb = arg0(&args);
3705            for (i, it) in items.iter().enumerate() {
3706                let m = host::invoke(
3707                    &cb,
3708                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3709                    None,
3710                )?;
3711                if with_host(|h| h.truthy(&m)) {
3712                    return Ok(Value::Float(i as f64));
3713                }
3714            }
3715            Ok(Value::Float(-1.0))
3716        }
3717        "some" => {
3718            let items = array_items(recv);
3719            let cb = arg0(&args);
3720            for (i, it) in items.iter().enumerate() {
3721                let m = host::invoke(
3722                    &cb,
3723                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3724                    None,
3725                )?;
3726                if with_host(|h| h.truthy(&m)) {
3727                    return Ok(Value::Bool(true));
3728                }
3729            }
3730            Ok(Value::Bool(false))
3731        }
3732        "every" => {
3733            let items = array_items(recv);
3734            let cb = arg0(&args);
3735            for (i, it) in items.iter().enumerate() {
3736                let m = host::invoke(
3737                    &cb,
3738                    vec![it.clone(), Value::Float(i as f64), recv.clone()],
3739                    None,
3740                )?;
3741                if !with_host(|h| h.truthy(&m)) {
3742                    return Ok(Value::Bool(false));
3743                }
3744            }
3745            Ok(Value::Bool(true))
3746        }
3747        "reduce" => {
3748            let items = array_items(recv);
3749            let cb = arg0(&args);
3750            let mut acc;
3751            let mut start = 0;
3752            if args.len() >= 2 {
3753                acc = args[1].clone();
3754            } else if !items.is_empty() {
3755                acc = items[0].clone();
3756                start = 1;
3757            } else {
3758                return Err(host::type_error(
3759                    "Reduce of empty array with no initial value",
3760                ));
3761            }
3762            for (i, it) in items.iter().enumerate().skip(start) {
3763                acc = host::invoke(
3764                    &cb,
3765                    vec![acc, it.clone(), Value::Float(i as f64), recv.clone()],
3766                    None,
3767                )?;
3768            }
3769            Ok(acc)
3770        }
3771        "reduceRight" => {
3772            let items = array_items(recv);
3773            let cb = arg0(&args);
3774            let n = items.len();
3775            let mut acc;
3776            let mut i = n; // one past the next index to process (walking down)
3777            if args.len() >= 2 {
3778                acc = args[1].clone();
3779            } else if n > 0 {
3780                acc = items[n - 1].clone();
3781                i = n - 1;
3782            } else {
3783                return Err(host::type_error(
3784                    "Reduce of empty array with no initial value",
3785                ));
3786            }
3787            while i > 0 {
3788                i -= 1;
3789                acc = host::invoke(
3790                    &cb,
3791                    vec![acc, items[i].clone(), Value::Float(i as f64), recv.clone()],
3792                    None,
3793                )?;
3794            }
3795            Ok(acc)
3796        }
3797        "findLast" => {
3798            let items = array_items(recv);
3799            let cb = arg0(&args);
3800            for i in (0..items.len()).rev() {
3801                let m = host::invoke(
3802                    &cb,
3803                    vec![items[i].clone(), Value::Float(i as f64), recv.clone()],
3804                    None,
3805                )?;
3806                if with_host(|h| h.truthy(&m)) {
3807                    return Ok(items[i].clone());
3808                }
3809            }
3810            Ok(Value::Undef)
3811        }
3812        "findLastIndex" => {
3813            let items = array_items(recv);
3814            let cb = arg0(&args);
3815            for i in (0..items.len()).rev() {
3816                let m = host::invoke(
3817                    &cb,
3818                    vec![items[i].clone(), Value::Float(i as f64), recv.clone()],
3819                    None,
3820                )?;
3821                if with_host(|h| h.truthy(&m)) {
3822                    return Ok(Value::Float(i as f64));
3823                }
3824            }
3825            Ok(Value::Float(-1.0))
3826        }
3827        "sort" => {
3828            let mut items = array_items(recv);
3829            sort_values(&mut items, args.first())?;
3830            with_host(|h| {
3831                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
3832                    *a = items;
3833                }
3834            });
3835            Ok(recv.clone())
3836        }
3837        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
3838        "toSorted" => {
3839            let mut items = array_items(recv);
3840            sort_values(&mut items, args.first())?;
3841            Ok(with_host(|h| h.new_array(items)))
3842        }
3843        "toReversed" => {
3844            let mut items = array_items(recv);
3845            items.reverse();
3846            Ok(with_host(|h| h.new_array(items)))
3847        }
3848        "toSpliced" => {
3849            let mut items = array_items(recv);
3850            let len = items.len();
3851            let start = {
3852                let s = arg_num(&args, 0);
3853                if s < 0.0 {
3854                    ((len as f64 + s).max(0.0)) as usize
3855                } else {
3856                    (s as usize).min(len)
3857                }
3858            };
3859            let delete = if args.len() >= 2 {
3860                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
3861            } else if args.is_empty() {
3862                0
3863            } else {
3864                len - start
3865            };
3866            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
3867            items.splice(start..start + delete, inserts);
3868            Ok(with_host(|h| h.new_array(items)))
3869        }
3870        "with" => {
3871            let mut items = array_items(recv);
3872            let len = items.len() as i64;
3873            let rel = arg_num(&args, 0) as i64;
3874            let idx = if rel < 0 { len + rel } else { rel };
3875            if idx < 0 || idx >= len {
3876                return Err(host::range_error(&format!("Invalid index : {rel}")));
3877            }
3878            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
3879            Ok(with_host(|h| h.new_array(items)))
3880        }
3881        "flat" => {
3882            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
3883            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
3884            let raw = if args.is_empty() {
3885                1.0
3886            } else {
3887                arg_num(&args, 0)
3888            };
3889            let depth = if raw.is_nan() {
3890                0.0
3891            } else if raw.is_infinite() {
3892                raw
3893            } else {
3894                raw.trunc()
3895            };
3896            let mut out = Vec::new();
3897            flatten_into(array_items(recv), depth, &mut out);
3898            Ok(with_host(|h| h.new_array(out)))
3899        }
3900        "keys" => {
3901            let n = array_items(recv).len();
3902            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
3903            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
3904        }
3905        "values" | "@@iterator" => {
3906            let items = array_items(recv);
3907            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
3908        }
3909        "entries" => {
3910            let items = array_items(recv);
3911            let pairs: Vec<Value> = items
3912                .into_iter()
3913                .enumerate()
3914                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
3915                .collect();
3916            Ok(with_host(|h| {
3917                h.alloc(JsObj::Iter {
3918                    items: pairs,
3919                    idx: 0,
3920                })
3921            }))
3922        }
3923        "splice" => array_splice(recv, args),
3924        "toString" => {
3925            let s = with_host(|h| h.str_of(recv));
3926            Ok(with_host(|h| h.new_str(s)))
3927        }
3928        _ => Err(host::type_error(&format!("{name} is not a function"))),
3929    }
3930}
3931
3932/// In-place sort of `items` (shared by `sort` and `toSorted`). Uses insertion
3933/// sort so the fallible JS comparator can be called; default order is by the
3934/// string form of each element. Propagates a comparator error.
3935fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
3936    for i in 1..items.len() {
3937        let mut j = i;
3938        while j > 0 {
3939            let order = match cmp {
3940                Some(cb) => {
3941                    let v = host::invoke(cb, vec![items[j - 1].clone(), items[j].clone()], None)?;
3942                    with_host(|h| h.to_number(&v))
3943                }
3944                None => {
3945                    let a = with_host(|h| h.str_of(&items[j - 1]));
3946                    let b = with_host(|h| h.str_of(&items[j]));
3947                    if a > b {
3948                        1.0
3949                    } else {
3950                        -1.0
3951                    }
3952                }
3953            };
3954            if order > 0.0 {
3955                items.swap(j - 1, j);
3956                j -= 1;
3957            } else {
3958                break;
3959            }
3960        }
3961    }
3962    Ok(())
3963}
3964
3965/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
3966/// f64 so `Infinity` (full flatten) and finite counts share one path.
3967fn flatten_into(items: Vec<Value>, depth: f64, out: &mut Vec<Value>) {
3968    for it in items {
3969        let inner = if depth > 0.0 {
3970            match with_host(|h| h.get(&it).cloned()) {
3971                Some(JsObj::Array(inner)) => Some(inner),
3972                _ => None,
3973            }
3974        } else {
3975            None
3976        };
3977        match inner {
3978            Some(inner) => flatten_into(inner, depth - 1.0, out),
3979            None => out.push(it),
3980        }
3981    }
3982}
3983
3984fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
3985    let len = array_items(recv).len();
3986    let start = {
3987        let s = arg_num(&args, 0);
3988        if s < 0.0 {
3989            ((len as f64 + s).max(0.0)) as usize
3990        } else {
3991            (s as usize).min(len)
3992        }
3993    };
3994    let delete = if args.len() >= 2 {
3995        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
3996    } else {
3997        len - start
3998    };
3999    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
4000    let removed = with_host(|h| {
4001        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
4002            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
4003            removed
4004        } else {
4005            Vec::new()
4006        }
4007    });
4008    Ok(with_host(|h| h.new_array(removed)))
4009}
4010
4011fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
4012    let norm = |v: f64| -> usize {
4013        if v < 0.0 {
4014            ((len as f64 + v).max(0.0)) as usize
4015        } else {
4016            (v as usize).min(len)
4017        }
4018    };
4019    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
4020        0
4021    } else {
4022        norm(arg_num(args, 0))
4023    };
4024    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
4025        len
4026    } else {
4027        norm(arg_num(args, 1))
4028    };
4029    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
4030    // never a reversed one: JS `slice` clamps `end` up to `start`.
4031    (lo, hi.max(lo))
4032}
4033
4034fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
4035    let chars: Vec<char> = s.chars().collect();
4036    match name {
4037        "@@iterator" => {
4038            let items: Vec<Value> = chars.iter().map(|c| new_s(c.to_string())).collect();
4039            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
4040        }
4041        "toUpperCase" => Ok(new_s(s.to_uppercase())),
4042        "toLowerCase" => Ok(new_s(s.to_lowercase())),
4043        // Locale comparison (ASCII approximation of ICU collation): primary by
4044        // case-folded order, then lowercase sorts before uppercase at a tie.
4045        "localeCompare" => {
4046            let other = with_host(|h| h.str_of(&arg0(&args)));
4047            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
4048            let r = match la.cmp(&lb) {
4049                std::cmp::Ordering::Less => -1.0,
4050                std::cmp::Ordering::Greater => 1.0,
4051                std::cmp::Ordering::Equal => {
4052                    let mut t = 0.0;
4053                    for (ca, cb) in s.chars().zip(other.chars()) {
4054                        if ca != cb {
4055                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
4056                            break;
4057                        }
4058                    }
4059                    t
4060                }
4061            };
4062            Ok(Value::Float(r))
4063        }
4064        "normalize" => Ok(new_s(s.to_string())),
4065        "trim" => Ok(new_s(s.trim().to_string())),
4066        "trimStart" => Ok(new_s(s.trim_start().to_string())),
4067        "trimEnd" => Ok(new_s(s.trim_end().to_string())),
4068        "toString" | "valueOf" => Ok(new_s(s.to_string())),
4069        "charAt" => {
4070            let i = arg_num(&args, 0) as usize;
4071            Ok(new_s(
4072                chars.get(i).map(|c| c.to_string()).unwrap_or_default(),
4073            ))
4074        }
4075        "at" => {
4076            let mut i = arg_num(&args, 0) as i64;
4077            if i < 0 {
4078                i += chars.len() as i64;
4079            }
4080            if i >= 0 && (i as usize) < chars.len() {
4081                Ok(new_s(chars[i as usize].to_string()))
4082            } else {
4083                Ok(Value::Undef)
4084            }
4085        }
4086        "charCodeAt" | "codePointAt" => {
4087            let i = arg_num(&args, 0) as usize;
4088            match chars.get(i) {
4089                Some(c) => Ok(Value::Float(*c as u32 as f64)),
4090                None => Ok(Value::Float(f64::NAN)),
4091            }
4092        }
4093        "indexOf" => {
4094            let needle = with_host(|h| h.str_of(&arg0(&args)));
4095            Ok(Value::Float(byte_to_char_index(s, s.find(&needle))))
4096        }
4097        "lastIndexOf" => {
4098            let needle = with_host(|h| h.str_of(&arg0(&args)));
4099            Ok(Value::Float(byte_to_char_index(s, s.rfind(&needle))))
4100        }
4101        "includes" => {
4102            let needle = with_host(|h| h.str_of(&arg0(&args)));
4103            Ok(Value::Bool(s.contains(&needle)))
4104        }
4105        "startsWith" => {
4106            let needle = with_host(|h| h.str_of(&arg0(&args)));
4107            Ok(Value::Bool(s.starts_with(&needle)))
4108        }
4109        "endsWith" => {
4110            let needle = with_host(|h| h.str_of(&arg0(&args)));
4111            Ok(Value::Bool(s.ends_with(&needle)))
4112        }
4113        "slice" => {
4114            let (lo, hi) = slice_bounds(&args, chars.len());
4115            Ok(new_s(chars[lo..hi].iter().collect()))
4116        }
4117        "substring" => {
4118            let mut a = arg_num(&args, 0).max(0.0) as usize;
4119            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
4120                chars.len()
4121            } else {
4122                (arg_num(&args, 1).max(0.0) as usize).min(chars.len())
4123            };
4124            a = a.min(chars.len());
4125            if a > b {
4126                std::mem::swap(&mut a, &mut b);
4127            }
4128            Ok(new_s(chars[a..b].iter().collect()))
4129        }
4130        "substr" => {
4131            // A negative start counts from the end: max(len + start, 0).
4132            let len = chars.len() as i64;
4133            let mut start = arg_num(&args, 0) as i64;
4134            if start < 0 {
4135                start = (len + start).max(0);
4136            }
4137            let start = (start as usize).min(chars.len());
4138            let count = if args.len() >= 2 {
4139                arg_num(&args, 1).max(0.0) as usize
4140            } else {
4141                chars.len()
4142            };
4143            let end = (start + count).min(chars.len());
4144            Ok(new_s(chars[start..end].iter().collect()))
4145        }
4146        "repeat" => {
4147            let n = arg_num(&args, 0);
4148            if n < 0.0 || !n.is_finite() {
4149                return Err(host::type_error("Invalid count value"));
4150            }
4151            Ok(new_s(s.repeat(n as usize)))
4152        }
4153        "concat" => {
4154            let mut out = s.to_string();
4155            for a in &args {
4156                out.push_str(&with_host(|h| h.str_of(a)));
4157            }
4158            Ok(new_s(out))
4159        }
4160        "padStart" => Ok(new_s(pad(s, &args, true))),
4161        "padEnd" => Ok(new_s(pad(s, &args, false))),
4162        // Regex-taking string methods: dispatch to the regexp module when the
4163        // argument is a RegExp; otherwise keep the plain-string behavior.
4164        "match" => crate::regexp::str_match(s, &arg0(&args)),
4165        "matchAll" => crate::regexp::str_match_all(s, &arg0(&args)),
4166        "search" => {
4167            if is_regexp_arg(&arg0(&args)) {
4168                crate::regexp::str_search(s, &arg0(&args))
4169            } else {
4170                // A string arg is coerced to a (literal) regex; we approximate with
4171                // a plain substring search, which agrees for non-metacharacter
4172                // needles.
4173                let needle = with_host(|h| h.str_of(&arg0(&args)));
4174                Ok(Value::Float(byte_to_char_index(s, s.find(&needle))))
4175            }
4176        }
4177        "replace" => {
4178            let pat = arg0(&args);
4179            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
4180            if is_regexp_arg(&pat) {
4181                crate::regexp::str_replace_regex(s, &pat, &repl, false)
4182            } else if with_host(|h| host::is_callable(h, &repl)) {
4183                Ok(new_s(replace_str_fn(
4184                    s,
4185                    &with_host(|h| h.str_of(&pat)),
4186                    &repl,
4187                    false,
4188                )?))
4189            } else {
4190                let from = with_host(|h| h.str_of(&pat));
4191                let to = with_host(|h| h.str_of(&repl));
4192                Ok(new_s(s.replacen(&from, &to, 1)))
4193            }
4194        }
4195        "replaceAll" => {
4196            let pat = arg0(&args);
4197            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
4198            if is_regexp_arg(&pat) {
4199                crate::regexp::str_replace_regex(s, &pat, &repl, true)
4200            } else if with_host(|h| host::is_callable(h, &repl)) {
4201                Ok(new_s(replace_str_fn(
4202                    s,
4203                    &with_host(|h| h.str_of(&pat)),
4204                    &repl,
4205                    true,
4206                )?))
4207            } else {
4208                let from = with_host(|h| h.str_of(&pat));
4209                let to = with_host(|h| h.str_of(&repl));
4210                Ok(new_s(s.replace(&from, &to)))
4211            }
4212        }
4213        "split" => {
4214            if is_regexp_arg(&arg0(&args)) {
4215                let limit = args
4216                    .get(1)
4217                    .filter(|v| !matches!(v, Value::Undef))
4218                    .map(|v| with_host(|h| h.to_number(v)) as usize);
4219                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
4220            }
4221            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
4222                vec![new_s(s.to_string())]
4223            } else {
4224                let sep = with_host(|h| h.str_of(&args[0]));
4225                if sep.is_empty() {
4226                    chars.iter().map(|c| new_s(c.to_string())).collect()
4227                } else {
4228                    s.split(&sep as &str)
4229                        .map(|p| new_s(p.to_string()))
4230                        .collect()
4231                }
4232            };
4233            // Optional limit: keep at most `limit` substrings.
4234            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
4235                let n = with_host(|h| h.to_number(lim));
4236                if n.is_finite() && n >= 0.0 {
4237                    parts.truncate(n as usize);
4238                }
4239            }
4240            Ok(with_host(|h| h.new_array(parts)))
4241        }
4242        _ => Err(host::type_error(&format!("{name} is not a function"))),
4243    }
4244}
4245
4246fn new_s(s: String) -> Value {
4247    with_host(|h| h.new_str(s))
4248}
4249
4250fn byte_to_char_index(s: &str, byte: Option<usize>) -> f64 {
4251    match byte {
4252        Some(b) => s[..b].chars().count() as f64,
4253        None => -1.0,
4254    }
4255}
4256
4257fn pad(s: &str, args: &[Value], start: bool) -> String {
4258    let target = arg_num(args, 0) as usize;
4259    let cur = s.chars().count();
4260    if cur >= target {
4261        return s.to_string();
4262    }
4263    let filler = if args.len() >= 2 {
4264        with_host(|h| h.str_of(&args[1]))
4265    } else {
4266        " ".to_string()
4267    };
4268    if filler.is_empty() {
4269        return s.to_string();
4270    }
4271    let need = target - cur;
4272    let fill_chars: Vec<char> = filler.chars().collect();
4273    let padding: String = (0..need)
4274        .map(|i| fill_chars[i % fill_chars.len()])
4275        .collect();
4276    if start {
4277        format!("{padding}{s}")
4278    } else {
4279        format!("{s}{padding}")
4280    }
4281}
4282
4283/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
4284fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
4285    match name {
4286        "toString" => {
4287            let radix = args.first().map(|_| arg_num(&args, 0) as u32).unwrap_or(10);
4288            if !(2..=36).contains(&radix) {
4289                return Err("RangeError: toString() radix must be between 2 and 36".into());
4290            }
4291            Ok(new_s(b.to_str_radix(radix)))
4292        }
4293        "toLocaleString" => Ok(new_s(b.to_string())),
4294        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
4295        _ => Err(host::type_error(&format!("{name} is not a function"))),
4296    }
4297}
4298
4299fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
4300    match name {
4301        "toFixed" => {
4302            let digits = arg_num(&args, 0).max(0.0) as usize;
4303            Ok(new_s(to_fixed(n, digits)))
4304        }
4305        "toString" => {
4306            let radix = args.first().map(|_| arg_num(&args, 0) as u32).unwrap_or(10);
4307            if radix == 10 || !(2..=36).contains(&radix) {
4308                Ok(new_s(host::fmt_number(n)))
4309            } else {
4310                Ok(new_s(to_radix(n, radix)))
4311            }
4312        }
4313        "toPrecision" => {
4314            if args.is_empty() {
4315                Ok(new_s(host::fmt_number(n)))
4316            } else {
4317                let p = arg_num(&args, 0) as usize;
4318                Ok(new_s(to_precision(n, p.max(1))))
4319            }
4320        }
4321        "toLocaleString" => Ok(new_s(to_locale_string(n))),
4322        "valueOf" => Ok(Value::Float(n)),
4323        _ => Err(host::type_error(&format!("{name} is not a function"))),
4324    }
4325}
4326
4327/// `Number.prototype.toLocaleString()` with the default locale and options:
4328/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
4329/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
4330/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
4331/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
4332/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
4333fn to_locale_string(n: f64) -> String {
4334    if n.is_nan() {
4335        return "NaN".to_string();
4336    }
4337    if n.is_infinite() {
4338        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
4339    }
4340    let neg = n.is_sign_negative();
4341    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
4342    // (and a bare trailing point). `to_fixed` rounds half away from zero.
4343    let fixed = to_fixed(n.abs(), 3);
4344    let trimmed = match fixed.split_once('.') {
4345        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
4346        None => fixed.as_str(),
4347    };
4348    let (int_part, frac_part) = match trimmed.split_once('.') {
4349        Some((i, f)) => (i, Some(f)),
4350        None => (trimmed, None),
4351    };
4352    let mut out = String::new();
4353    if neg {
4354        out.push('-'); // Intl keeps the sign even for -0.
4355    }
4356    out.push_str(&group_thousands(int_part));
4357    if let Some(f) = frac_part {
4358        out.push('.');
4359        out.push_str(f);
4360    }
4361    out
4362}
4363
4364/// Insert `,` as a thousands separator into a nonnegative integer digit string.
4365fn group_thousands(int_part: &str) -> String {
4366    let bytes = int_part.as_bytes();
4367    let n = bytes.len();
4368    let mut out = String::with_capacity(n + n / 3);
4369    for (i, &b) in bytes.iter().enumerate() {
4370        if i > 0 && (n - i) % 3 == 0 {
4371            out.push(',');
4372        }
4373        out.push(b as char);
4374    }
4375    out
4376}
4377
4378/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
4379/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
4380/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
4381/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
4382///
4383/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
4384/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
4385/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
4386fn to_fixed(n: f64, f: usize) -> String {
4387    if !n.is_finite() {
4388        return host::fmt_number(n);
4389    }
4390    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
4391    if n.abs() >= 1e21 {
4392        return host::fmt_number(n);
4393    }
4394    let neg = n < 0.0;
4395    // Exact decimal with guard digits past the rounding position; then round the
4396    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
4397    let full = format!("{:.*}", f + 25, n.abs());
4398    let mut body = round_decimal_string(&full, f);
4399    if neg {
4400        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
4401    }
4402    body
4403}
4404
4405/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
4406/// fractional digits, half away from zero, propagating carry across the point.
4407fn round_decimal_string(s: &str, f: usize) -> String {
4408    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
4409    let mut digits: Vec<u8> = int_part
4410        .bytes()
4411        .chain(frac_part.bytes())
4412        .map(|b| b - b'0')
4413        .collect();
4414    let point = int_part.len(); // digits before the decimal point
4415    let keep = point + f; // number of leading digits to keep
4416
4417    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
4418    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
4419        let mut i = keep;
4420        loop {
4421            if i == 0 {
4422                digits.insert(0, 1);
4423                // A new leading digit shifts the decimal point right by one.
4424                return assemble_decimal(&digits, point + 1, f);
4425            }
4426            i -= 1;
4427            if digits[i] == 9 {
4428                digits[i] = 0;
4429            } else {
4430                digits[i] += 1;
4431                break;
4432            }
4433        }
4434    }
4435    assemble_decimal(&digits, point, f)
4436}
4437
4438/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
4439/// `point` digits precede the decimal point.
4440fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
4441    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
4442    let int_str = int_str.trim_start_matches('0');
4443    let int_str = if int_str.is_empty() { "0" } else { int_str };
4444    if f == 0 {
4445        return int_str.to_string();
4446    }
4447    let frac: String = digits[point..point + f]
4448        .iter()
4449        .map(|d| (d + b'0') as char)
4450        .collect();
4451    format!("{int_str}.{frac}")
4452}
4453
4454/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
4455/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
4456/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
4457/// retained (`(100).toPrecision(5) === "100.00"`).
4458fn to_precision(n: f64, p: usize) -> String {
4459    if !n.is_finite() {
4460        return host::fmt_number(n);
4461    }
4462    if n == 0.0 {
4463        return if p == 1 {
4464            "0".into()
4465        } else {
4466            format!("0.{}", "0".repeat(p - 1))
4467        };
4468    }
4469    let neg = n < 0.0;
4470    let a = n.abs();
4471    // Take the EXACT digits with guard positions past the p-th, then round to p
4472    // significant digits half away from zero — Rust's `{:.*e}` rounds half to
4473    // EVEN (`(2.5).toPrecision(1)` would give "2"), but JS rounds half up ("3").
4474    let sci = format!("{a:.*e}", p - 1 + 25);
4475    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
4476    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
4477    let all: Vec<u8> = mant
4478        .chars()
4479        .filter(|c| c.is_ascii_digit())
4480        .map(|c| c as u8 - b'0')
4481        .collect();
4482    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
4483    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
4484        // Round the p-digit mantissa up, propagating carry; a carry out of the
4485        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
4486        let mut d: Vec<u8> = all[..p].to_vec();
4487        let mut i = p;
4488        loop {
4489            if i == 0 {
4490                d.insert(0, 1);
4491                d.truncate(p);
4492                e += 1;
4493                break;
4494            }
4495            i -= 1;
4496            if d[i] == 9 {
4497                d[i] = 0;
4498            } else {
4499                d[i] += 1;
4500                break;
4501            }
4502        }
4503        s = d.iter().map(|x| (x + b'0') as char).collect();
4504    }
4505    let pp = p as i32;
4506
4507    let body = if e < -6 || e >= pp {
4508        // Exponential: first digit, optional '.rest', signed exponent.
4509        let sign = if e >= 0 { '+' } else { '-' };
4510        let mag = e.abs();
4511        if p == 1 {
4512            format!("{s}e{sign}{mag}")
4513        } else {
4514            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
4515        }
4516    } else if e >= 0 {
4517        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
4518        let ip = (e + 1) as usize;
4519        if ip == p {
4520            s
4521        } else {
4522            format!("{}.{}", &s[..ip], &s[ip..])
4523        }
4524    } else {
4525        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
4526        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
4527    };
4528    if neg {
4529        format!("-{body}")
4530    } else {
4531        body
4532    }
4533}
4534
4535/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
4536/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
4537/// is emitted exact, and fractional digits are produced up to the input double's
4538/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
4539/// carry-over back into already-written digits (and into the integer part).
4540fn to_radix(n: f64, radix: u32) -> String {
4541    if !n.is_finite() {
4542        return host::fmt_number(n);
4543    }
4544    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
4545    let rf = radix as f64;
4546    let neg = n < 0.0;
4547    let value = n.abs();
4548
4549    let mut integer = value.floor();
4550    let mut fraction = value - integer;
4551
4552    // Fraction digits, most-significant first.
4553    let mut frac: Vec<u8> = Vec::new();
4554    // Only compute fractional digits down to the input double's precision.
4555    let mut delta = 0.5 * (next_up(value) - value);
4556    delta = delta.max(next_up(0.0));
4557    if fraction >= delta {
4558        loop {
4559            // Shift up by one digit.
4560            fraction *= rf;
4561            delta *= rf;
4562            let digit = fraction as usize;
4563            frac.push(digits[digit]);
4564            fraction -= digit as f64;
4565            // Round to even.
4566            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
4567                // Carry-over: back-trace already-written fraction digits.
4568                loop {
4569                    match frac.pop() {
4570                        None => {
4571                            // Carried past the point into the integer part.
4572                            integer += 1.0;
4573                            break;
4574                        }
4575                        Some(c) => {
4576                            let d = if c > b'9' {
4577                                (c - b'a' + 10) as u32
4578                            } else {
4579                                (c - b'0') as u32
4580                            };
4581                            if d + 1 < radix {
4582                                frac.push(digits[(d + 1) as usize]);
4583                                break;
4584                            }
4585                            // digit was radix-1: drop it and keep carrying.
4586                        }
4587                    }
4588                }
4589                break;
4590            }
4591            if fraction < delta {
4592                break;
4593            }
4594        }
4595    }
4596
4597    // Integer digits, least-significant first (reversed at the end).
4598    let mut int_out: Vec<u8> = Vec::new();
4599    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
4600    while v8_exponent(integer / rf) > 0 {
4601        integer /= rf;
4602        int_out.push(b'0');
4603    }
4604    loop {
4605        let remainder = integer % rf;
4606        int_out.push(digits[remainder as usize]);
4607        integer = (integer - remainder) / rf;
4608        if integer <= 0.0 {
4609            break;
4610        }
4611    }
4612    int_out.reverse();
4613
4614    let mut out: Vec<u8> = Vec::new();
4615    if neg {
4616        out.push(b'-');
4617    }
4618    out.extend_from_slice(&int_out);
4619    if !frac.is_empty() {
4620        out.push(b'.');
4621        out.extend_from_slice(&frac);
4622    }
4623    String::from_utf8(out).unwrap()
4624}
4625
4626/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
4627fn next_up(x: f64) -> f64 {
4628    f64::from_bits(x.to_bits() + 1)
4629}
4630
4631/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
4632/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
4633fn v8_exponent(x: f64) -> i32 {
4634    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
4635    if biased == 0 {
4636        -1074 // denormal
4637    } else {
4638        biased - 1075
4639    }
4640}
4641
4642// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
4643
4644fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
4645    match name {
4646        "get" => {
4647            let key = with_host(|h| host::map_key(h, &arg0(&args)));
4648            Ok(with_host(|h| match h.get(recv) {
4649                Some(JsObj::Map { entries, .. }) => entries
4650                    .get(&key)
4651                    .map(|(_, v)| v.clone())
4652                    .unwrap_or(Value::Undef),
4653                _ => Value::Undef,
4654            }))
4655        }
4656        "set" => {
4657            let kv = arg0(&args);
4658            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
4659            let key = with_host(|h| host::map_key(h, &kv));
4660            with_host(|h| {
4661                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
4662                    entries.insert(key, (kv, vv));
4663                }
4664            });
4665            Ok(recv.clone())
4666        }
4667        "has" => {
4668            let key = with_host(|h| host::map_key(h, &arg0(&args)));
4669            Ok(Value::Bool(with_host(
4670                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
4671            )))
4672        }
4673        "delete" => {
4674            let key = with_host(|h| host::map_key(h, &arg0(&args)));
4675            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
4676                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
4677                _ => false,
4678            })))
4679        }
4680        "clear" => {
4681            with_host(|h| {
4682                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
4683                    entries.clear();
4684                }
4685            });
4686            Ok(Value::Undef)
4687        }
4688        "forEach" => {
4689            let cb = arg0(&args);
4690            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
4691                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
4692                _ => Vec::new(),
4693            });
4694            for (k, v) in pairs {
4695                host::invoke(&cb, vec![v, k, recv.clone()], None)?;
4696            }
4697            Ok(Value::Undef)
4698        }
4699        "keys" | "values" | "entries" | "@@iterator" => {
4700            let items: Vec<Value> = with_host(|h| {
4701                let pairs: Vec<(Value, Value)> = match h.get(recv) {
4702                    Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
4703                    _ => Vec::new(),
4704                };
4705                pairs
4706                    .into_iter()
4707                    .map(|(k, v)| match name {
4708                        "keys" => k,
4709                        "values" => v,
4710                        _ => h.new_array(vec![k, v]), // entries + @@iterator
4711                    })
4712                    .collect()
4713            });
4714            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
4715        }
4716        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
4717    }
4718}
4719
4720fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
4721    match name {
4722        "add" => {
4723            let vv = arg0(&args);
4724            let key = with_host(|h| host::map_key(h, &vv));
4725            with_host(|h| {
4726                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
4727                    entries.insert(key, vv);
4728                }
4729            });
4730            Ok(recv.clone())
4731        }
4732        "has" => {
4733            let key = with_host(|h| host::map_key(h, &arg0(&args)));
4734            Ok(Value::Bool(with_host(
4735                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
4736            )))
4737        }
4738        "delete" => {
4739            let key = with_host(|h| host::map_key(h, &arg0(&args)));
4740            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
4741                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
4742                _ => false,
4743            })))
4744        }
4745        "clear" => {
4746            with_host(|h| {
4747                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
4748                    entries.clear();
4749                }
4750            });
4751            Ok(Value::Undef)
4752        }
4753        "forEach" => {
4754            let cb = arg0(&args);
4755            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
4756                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
4757                _ => Vec::new(),
4758            });
4759            for v in vals {
4760                host::invoke(&cb, vec![v.clone(), v, recv.clone()], None)?;
4761            }
4762            Ok(Value::Undef)
4763        }
4764        "keys" | "values" | "entries" | "@@iterator" => {
4765            let items: Vec<Value> = with_host(|h| {
4766                let vals: Vec<Value> = match h.get(recv) {
4767                    Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
4768                    _ => Vec::new(),
4769                };
4770                if name == "entries" {
4771                    vals.into_iter()
4772                        .map(|v| h.new_array(vec![v.clone(), v]))
4773                        .collect()
4774                } else {
4775                    vals
4776                }
4777            });
4778            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
4779        }
4780        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
4781    }
4782}
4783
4784fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
4785    match name {
4786        "next" => {
4787            let send = arg0(&args);
4788            match host::gen_resume(recv, send)? {
4789                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
4790                host::GenStep::Done(v) => Ok(iter_result(v, true)),
4791            }
4792        }
4793        "return" => {
4794            // Resume with an injected return so any pending `finally` runs; the
4795            // completion may itself be a `finally` yield (not-done) or the value.
4796            match host::gen_return(recv, arg0(&args))? {
4797                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
4798                host::GenStep::Done(v) => Ok(iter_result(v, true)),
4799            }
4800        }
4801        "throw" => {
4802            // Inject a throw at the suspension point: an enclosing `try/catch` in
4803            // the body can handle it (and any `finally` runs); otherwise it
4804            // propagates to the caller.
4805            match host::gen_throw(recv, arg0(&args))? {
4806                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
4807                host::GenStep::Done(v) => Ok(iter_result(v, true)),
4808            }
4809        }
4810        _ => Err(host::type_error(&format!(
4811            "generator.{name} is not a function"
4812        ))),
4813    }
4814}
4815
4816/// A `{ value, done }` iterator-result object.
4817fn iter_result(value: Value, done: bool) -> Value {
4818    with_host(|h| {
4819        let mut m: IndexMap<String, Value> = IndexMap::new();
4820        m.insert("value".into(), value);
4821        m.insert("done".into(), Value::Bool(done));
4822        h.new_object(m)
4823    })
4824}
4825
4826/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
4827/// cursor over a materialized item list.
4828fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
4829    match name {
4830        "next" => {
4831            let step = with_host(|h| {
4832                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
4833                    if *idx < items.len() {
4834                        let v = items[*idx].clone();
4835                        *idx += 1;
4836                        return Some(v);
4837                    }
4838                }
4839                None
4840            });
4841            Ok(match step {
4842                Some(v) => iter_result(v, false),
4843                None => iter_result(Value::Undef, true),
4844            })
4845        }
4846        "return" => {
4847            // Exhaust the cursor and report done.
4848            with_host(|h| {
4849                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
4850                    *idx = items.len();
4851                }
4852            });
4853            Ok(iter_result(arg0(&args), true))
4854        }
4855        // An iterator is its own iterable.
4856        "@@iterator" => Ok(recv.clone()),
4857        _ => Err(host::type_error(&format!(
4858            "iterator.{name} is not a function"
4859        ))),
4860    }
4861}
4862
4863fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
4864    match name {
4865        "toString" => Ok(with_host(|h| {
4866            let s = h.str_of(recv);
4867            h.new_str(s)
4868        })),
4869        _ => Err(host::type_error(&format!(
4870            "symbol.{name} is not a function"
4871        ))),
4872    }
4873}
4874
4875// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
4876
4877fn object_create(args: Vec<Value>) -> Result<Value, String> {
4878    let proto = arg0(&args);
4879    let obj = with_host(|h| h.new_object(IndexMap::new()));
4880    // `set_proto` records a null proto as an explicit null-prototype object;
4881    // undefined leaves the object with the default (bare-object) prototype.
4882    if !matches!(proto, Value::Undef) {
4883        with_host(|h| h.set_proto(&obj, proto));
4884    }
4885    // Optional second arg: a property-descriptor map.
4886    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
4887        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
4888            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
4889            _ => Vec::new(),
4890        });
4891        for (k, d) in entries {
4892            apply_descriptor(&obj, &k, &d);
4893        }
4894    }
4895    Ok(obj)
4896}
4897
4898/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
4899/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
4900/// `EventEmitter.prototype` (the one express mixes onto its app function).
4901fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
4902    match ns {
4903        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
4904        _ => None,
4905    }
4906}
4907
4908fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
4909    let obj = arg0(&args);
4910    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
4911    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
4912    apply_descriptor(&obj, &key, &desc);
4913    Ok(obj)
4914}
4915
4916/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
4917fn apply_descriptor(obj: &Value, key: &str, desc: &Value) {
4918    let (value, get, set) = with_host(|h| match h.get(desc) {
4919        Some(JsObj::Object(p)) => (
4920            p.get("value").cloned(),
4921            p.get("get").cloned(),
4922            p.get("set").cloned(),
4923        ),
4924        _ => (None, None, None),
4925    });
4926    if get.is_some() || set.is_some() {
4927        with_host(|h| h.set_accessor(obj, key, get, set));
4928    } else if let Some(v) = value {
4929        // A function/class receiver stores its own props in the fn-prop side table
4930        // (express `mixin(app, proto)` defines methods onto the `app` *function*).
4931        if matches!(
4932            with_host(|h| h.get(obj).cloned()),
4933            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
4934        ) {
4935            with_host(|h| h.set_fn_prop(obj, key, v));
4936        } else {
4937            with_host(|h| {
4938                if let Some(JsObj::Object(p)) = h.get_mut(obj) {
4939                    p.insert(key.to_string(), v);
4940                    host::canonicalize_own_keys(p);
4941                }
4942            });
4943        }
4944    }
4945}
4946
4947fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
4948    let obj = arg0(&args);
4949    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
4950    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
4951    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
4952    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
4953        if let Some(names) = builtin_proto_method_names(&ns) {
4954            if names.contains(&key.as_str()) {
4955                return Ok(with_host(|h| {
4956                    let thunk = h.alloc(JsObj::Builtin(format!(
4957                        "@proto:{}:{key}",
4958                        ns.trim_end_matches(".prototype")
4959                    )));
4960                    let mut m: IndexMap<String, Value> = IndexMap::new();
4961                    m.insert("value".into(), thunk);
4962                    m.insert("writable".into(), Value::Bool(true));
4963                    m.insert("enumerable".into(), Value::Bool(true));
4964                    m.insert("configurable".into(), Value::Bool(true));
4965                    h.new_object(m)
4966                }));
4967            }
4968        }
4969    }
4970    // Accessor descriptor?
4971    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
4972        return Ok(with_host(|h| {
4973            let mut m: IndexMap<String, Value> = IndexMap::new();
4974            m.insert("get".into(), get.unwrap_or(Value::Undef));
4975            m.insert("set".into(), set.unwrap_or(Value::Undef));
4976            m.insert("enumerable".into(), Value::Bool(true));
4977            m.insert("configurable".into(), Value::Bool(true));
4978            h.new_object(m)
4979        }));
4980    }
4981    let val = with_host(|h| match h.get(&obj) {
4982        Some(JsObj::Object(p)) => p.get(&key).cloned(),
4983        // A function/class own prop lives in the fn-prop side table.
4984        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
4985        _ => None,
4986    });
4987    match val {
4988        Some(v) => Ok(with_host(|h| {
4989            let mut m: IndexMap<String, Value> = IndexMap::new();
4990            m.insert("value".into(), v);
4991            m.insert("writable".into(), Value::Bool(true));
4992            m.insert("enumerable".into(), Value::Bool(true));
4993            m.insert("configurable".into(), Value::Bool(true));
4994            h.new_object(m)
4995        })),
4996        None => Ok(Value::Undef),
4997    }
4998}
4999
5000/// `key in obj` respecting the prototype chain.
5001pub fn has_property(obj: &Value, key: &str) -> bool {
5002    // `key in <builtin namespace/prototype>`: membership matches what a property
5003    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
5004    // prototype methods) resolve as callable thunks via `namespace_property`, so
5005    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
5006    // with the `in` operator before reading the intrinsic).
5007    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
5008        return !matches!(namespace_property(&ns, key), Value::Undef);
5009    }
5010    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
5011        return true;
5012    }
5013    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
5014        return true;
5015    }
5016    with_host(|h| match h.get(obj) {
5017        Some(JsObj::Object(p)) => p.contains_key(key),
5018        Some(JsObj::Array(items)) => {
5019            key == "length"
5020                || key
5021                    .parse::<usize>()
5022                    .map(|i| i < items.len())
5023                    .unwrap_or(false)
5024        }
5025        _ => false,
5026    })
5027}
5028
5029/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
5030fn deep_clone(v: &Value) -> Value {
5031    match with_host(|h| h.get(v).cloned()) {
5032        Some(JsObj::Array(items)) => {
5033            let cloned: Vec<Value> = items.iter().map(deep_clone).collect();
5034            with_host(|h| h.new_array(cloned))
5035        }
5036        Some(JsObj::Object(props)) => {
5037            let cloned: IndexMap<String, Value> = props
5038                .iter()
5039                .map(|(k, val)| (k.clone(), deep_clone(val)))
5040                .collect();
5041            with_host(|h| h.new_object(cloned))
5042        }
5043        _ => v.clone(),
5044    }
5045}
5046
5047// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
5048
5049/// A short `Name: message` string for an error value (used when an await
5050/// rejection unwinds as a thrown error).
5051pub fn error_string(h: &host::JsHost, v: &Value) -> String {
5052    if let Some(JsObj::Object(props)) = h.get(v) {
5053        let name = props
5054            .get("name")
5055            .map(|x| h.str_of(x))
5056            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
5057            .unwrap_or_else(|| "Error".into());
5058        if let Some(m) = props.get("message") {
5059            return format!("{name}: {}", h.str_of(m));
5060        }
5061        return name;
5062    }
5063    h.str_of(v)
5064}
5065
5066fn make_builtin(name: String) -> Value {
5067    with_host(|h| h.alloc(JsObj::Builtin(name)))
5068}
5069
5070/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
5071/// internal resolve/reject functions.
5072fn new_promise(executor: Value) -> Result<Value, String> {
5073    let p = with_host(|h| h.new_promise());
5074    let id = with_host(|h| h.promise_id(&p).unwrap());
5075    let res = make_builtin(format!("@@presolve:{id}"));
5076    let rej = make_builtin(format!("@@preject:{id}"));
5077    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
5078        // A throw in the executor rejects the promise.
5079        let ev = host::take_exc_or_error(&e);
5080        host::reject_promise_val(id, ev);
5081    }
5082    Ok(p)
5083}
5084
5085fn promise_resolve(v: Value) -> Result<Value, String> {
5086    Ok(host::promise_of(&v))
5087}
5088fn promise_reject(v: Value) -> Result<Value, String> {
5089    let p = with_host(|h| h.new_promise());
5090    let id = with_host(|h| h.promise_id(&p).unwrap());
5091    host::reject_promise_val(id, v);
5092    with_host(|h| h.promise_mark_handled(id)); // avoid spurious unhandled noise
5093    Ok(p)
5094}
5095
5096/// `Promise.withResolvers()` — a fresh pending promise paired with its own
5097/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
5098/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
5099fn promise_with_resolvers() -> Result<Value, String> {
5100    let p = with_host(|h| h.new_promise());
5101    let id = with_host(|h| h.promise_id(&p).unwrap());
5102    let resolve = make_builtin(format!("@@presolve:{id}"));
5103    let reject = make_builtin(format!("@@preject:{id}"));
5104    let mut props: IndexMap<String, Value> = IndexMap::new();
5105    props.insert("promise".into(), p);
5106    props.insert("resolve".into(), resolve);
5107    props.insert("reject".into(), reject);
5108    Ok(with_host(|h| h.new_object(props)))
5109}
5110
5111#[derive(Clone, Copy)]
5112enum AllMode {
5113    All,
5114    AllSettled,
5115}
5116
5117/// `Promise.all` / `Promise.allSettled`.
5118fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
5119    let items = host::iter_all(&arg0(&args))?;
5120    let result = with_host(|h| h.new_promise());
5121    let rid = with_host(|h| h.promise_id(&result).unwrap());
5122    let n = items.len();
5123    if n == 0 {
5124        let empty = with_host(|h| h.new_array(Vec::new()));
5125        host::resolve_promise_val(rid, empty);
5126        return Ok(result);
5127    }
5128    // Shared mutable accumulator via Rc<RefCell<…>>.
5129    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
5130    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
5131    for (i, it) in items.into_iter().enumerate() {
5132        let ap = host::promise_of(&it);
5133        let aid = with_host(|h| h.promise_id(&ap).unwrap());
5134        let slots = slots.clone();
5135        let remaining = remaining.clone();
5136        host::subscribe_native(
5137            aid,
5138            Box::new(move |state, val| {
5139                let settled = match mode {
5140                    AllMode::All => {
5141                        if state == host::PromiseState::Rejected {
5142                            host::reject_promise_val(rid, val);
5143                            return Ok(());
5144                        }
5145                        val
5146                    }
5147                    AllMode::AllSettled => with_host(|h| {
5148                        let mut m: IndexMap<String, Value> = IndexMap::new();
5149                        if state == host::PromiseState::Rejected {
5150                            m.insert("status".into(), h.new_str("rejected"));
5151                            m.insert("reason".into(), val);
5152                        } else {
5153                            m.insert("status".into(), h.new_str("fulfilled"));
5154                            m.insert("value".into(), val);
5155                        }
5156                        h.new_object(m)
5157                    }),
5158                };
5159                slots.borrow_mut()[i] = settled;
5160                let mut r = remaining.borrow_mut();
5161                *r -= 1;
5162                if *r == 0 {
5163                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
5164                    host::resolve_promise_val(rid, arr);
5165                }
5166                Ok(())
5167            }),
5168        );
5169    }
5170    Ok(result)
5171}
5172
5173/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
5174fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
5175    let items = host::iter_all(&arg0(&args))?;
5176    let result = with_host(|h| h.new_promise());
5177    let rid = with_host(|h| h.promise_id(&result).unwrap());
5178    let n = items.len();
5179    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
5180    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
5181    for (i, it) in items.into_iter().enumerate() {
5182        let ap = host::promise_of(&it);
5183        let aid = with_host(|h| h.promise_id(&ap).unwrap());
5184        let errors = errors.clone();
5185        let remaining = remaining.clone();
5186        host::subscribe_native(
5187            aid,
5188            Box::new(move |state, val| {
5189                if any {
5190                    if state == host::PromiseState::Fulfilled {
5191                        host::resolve_promise_val(rid, val);
5192                    } else {
5193                        errors.borrow_mut()[i] = val;
5194                        let mut r = remaining.borrow_mut();
5195                        *r -= 1;
5196                        if *r == 0 {
5197                            // All rejected → AggregateError (simplified to an Error).
5198                            let agg = with_host(|h| {
5199                                synth_error(h, "AggregateError: All promises were rejected")
5200                            });
5201                            host::reject_promise_val(rid, agg);
5202                        }
5203                    }
5204                } else if state == host::PromiseState::Rejected {
5205                    host::reject_promise_val(rid, val);
5206                } else {
5207                    host::resolve_promise_val(rid, val);
5208                }
5209                Ok(())
5210            }),
5211        );
5212    }
5213    Ok(result)
5214}
5215
5216/// `.then` / `.catch` / `.finally` on a promise.
5217fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
5218    match name {
5219        "then" => Ok(host::promise_then(
5220            recv,
5221            args.first().cloned().unwrap_or(Value::Undef),
5222            args.get(1).cloned().unwrap_or(Value::Undef),
5223        )),
5224        "catch" => Ok(host::promise_then(
5225            recv,
5226            Value::Undef,
5227            args.first().cloned().unwrap_or(Value::Undef),
5228        )),
5229        "finally" => {
5230            let cb = arg0(&args);
5231            let i = match cb {
5232                Value::Obj(i) => i,
5233                _ => 0,
5234            };
5235            let pass = make_builtin(format!("@@finpass:{i}"));
5236            let throw = make_builtin(format!("@@finthrow:{i}"));
5237            Ok(host::promise_then(recv, pass, throw))
5238        }
5239        _ => Err(host::type_error(&format!(
5240            "promise.{name} is not a function"
5241        ))),
5242    }
5243}
5244
5245fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
5246    with_host(|h| {
5247        if next_tick {
5248            h.queue_nexttick(cb, args);
5249        } else {
5250            h.queue_micro(cb, args);
5251        }
5252    });
5253}
5254
5255/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask. We do NOT
5256/// implement repeating intervals (each fires once) to keep output deterministic
5257/// and terminating; the delay orders timers on a virtual clock.
5258fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
5259    let cb = arg0(&args);
5260    let delay = if name == "setImmediate" {
5261        -1.0 // before any 0ms timeout
5262    } else {
5263        args.get(1)
5264            .map(|d| with_host(|h| h.to_number(d)))
5265            .unwrap_or(0.0)
5266            .max(0.0)
5267    };
5268    let extra = if name == "setImmediate" {
5269        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
5270    } else {
5271        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
5272    };
5273    let id = with_host(|h| h.add_timer(delay, cb, extra));
5274    Value::Float(id as f64)
5275}
5276
5277fn clear_timer(v: &Value) {
5278    let id = with_host(|h| h.to_number(v)) as u64;
5279    with_host(|h| h.cancel_timer(id));
5280}