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, ObjKind};
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::SETLOCAL_STRICT, b_setlocal_strict);
15    vm.register_builtin(ops::DECLARE, b_declare);
16    vm.register_builtin(ops::DECLARE_CONST, b_declare_const);
17    vm.register_builtin(ops::MARK_HOLE, b_mark_hole);
18    vm.register_builtin(ops::DELNAME, b_delname);
19    vm.register_builtin(ops::GETATTR, b_getattr);
20    vm.register_builtin(ops::SETATTR, b_setattr);
21    vm.register_builtin(ops::GETITEM, b_getitem);
22    vm.register_builtin(ops::SETITEM, b_setitem);
23    vm.register_builtin(ops::DELITEM, b_delitem);
24    vm.register_builtin(ops::MKSTR, b_mkstr);
25    vm.register_builtin(ops::MKARR, b_mkarr);
26    vm.register_builtin(ops::MKOBJ, b_mkobj);
27    vm.register_builtin(ops::CALL, b_call);
28    vm.register_builtin(ops::CALL_METHOD, b_call_method);
29    vm.register_builtin(ops::CALL_VALUE, b_call_value);
30    vm.register_builtin(ops::NEW, b_new);
31    vm.register_builtin(ops::TRUTHY, b_truthy);
32    vm.register_builtin(ops::TOSTR, b_tostr);
33    vm.register_builtin(ops::MKFUNC, b_mkfunc);
34    vm.register_builtin(ops::GETITER, b_getiter);
35    vm.register_builtin(ops::FORITER, b_foriter);
36    vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
37    vm.register_builtin(ops::CONTAINS, b_contains);
38    vm.register_builtin(ops::SIG_RETURN, b_sig_return);
39    vm.register_builtin(ops::BINOP, b_binop);
40    vm.register_builtin(ops::UNARY, b_unary);
41    vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
42    vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
43    vm.register_builtin(ops::TYPEOF, b_typeof);
44    vm.register_builtin(ops::LOAD_NULL, b_load_null);
45    vm.register_builtin(ops::THROW, b_throw);
46    vm.register_builtin(ops::TRY, b_try);
47    vm.register_builtin(ops::NULLISH, b_nullish);
48    vm.register_builtin(ops::UNPACK, b_unpack);
49    vm.register_builtin(ops::BUILD_ARGS, b_build_args);
50    vm.register_builtin(ops::THIS, b_this);
51    vm.register_builtin(ops::INSTANCEOF, b_instanceof);
52    vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
53    vm.register_builtin(ops::APPLY, b_apply);
54    vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
55    vm.register_builtin(ops::OBJ_REST, b_obj_rest);
56    vm.register_builtin(ops::DIV, b_div);
57    vm.register_builtin(ops::POW, b_pow);
58    vm.register_builtin(ops::MKCLASS, b_mkclass);
59    vm.register_builtin(ops::DEF_MEMBER, b_def_member);
60    vm.register_builtin(ops::DEF_FIELD, b_def_field);
61    vm.register_builtin(ops::SUPER_CALL, b_super_call);
62    vm.register_builtin(ops::SUPER_GET, b_super_get);
63    vm.register_builtin(ops::YIELD, b_yield);
64    vm.register_builtin(ops::PROPKEY, b_propkey);
65    vm.register_builtin(ops::NEW_TARGET, b_new_target);
66    vm.register_builtin(ops::AWAIT, b_await);
67    vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
68    vm.register_builtin(ops::DBG_LINE, b_dbg_line);
69    vm.register_builtin(ops::MKBIGINT, b_mkbigint);
70    vm.register_builtin(ops::MKREGEX, b_mkregex);
71    vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
72    vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
73    vm.register_builtin(ops::ASYNC_STEP, b_async_step);
74    vm.register_builtin(ops::NUM_STEP, b_num_step);
75    vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
76    vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
77    vm.register_builtin(ops::SIG_BREAK, b_sig_break);
78    vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
79    vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
80    vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
81    vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
82    vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
83    vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
84    vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
85}
86
87/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
88/// runs its pending `finally`; a user iterator object gets its `.return()` called
89/// if present; a plain materialized iterator just drops. Returns `undefined`.
90/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
91/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
92/// is neither is left alone.
93pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
94    if with_host(|h| h.is_generator_val(it)) {
95        host::gen_return(it, Value::Undef)?;
96        return Ok(());
97    }
98    if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
99        if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
100            if with_host(|h| host::is_callable(h, &f)) {
101                host::invoke(&f, Vec::new(), Some(it.clone()))?;
102            }
103        }
104    }
105    Ok(())
106}
107
108fn b_iter_close(vm: &mut VM, _: u8) -> Value {
109    let it = vm.pop();
110    // A `finally` may print or yield, but the loop is done either way; an error
111    // it raises still propagates.
112    match close_iterator(&it) {
113        Ok(()) => Value::Undef,
114        Err(e) => abort(vm, e),
115    }
116}
117
118/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
119/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
120/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
121/// Number nor throws the mix error.
122fn b_num_step(vm: &mut VM, _: u8) -> Value {
123    let old = vm.pop();
124    let tag = match vm.pop() {
125        Value::Int(n) => n,
126        Value::Float(f) => f as i64,
127        _ => 1,
128    };
129    if with_host(|h| h.is_bigint_val(&old)) {
130        let b = with_host(|h| h.as_bigint(&old)).unwrap();
131        let old_n = with_host(|h| h.new_bigint(b.clone()));
132        let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
133        vm.push(old_n);
134        new
135    } else {
136        let n = with_host(|h| h.to_number(&old));
137        vm.push(Value::Float(n));
138        Value::Float(n + tag as f64)
139    }
140}
141
142/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
143/// `{value, done}` record (see `host::async_step`).
144fn b_async_step(vm: &mut VM, _: u8) -> Value {
145    let iter = vm.pop();
146    let r = host::async_step(&iter);
147    finish(vm, r)
148}
149
150/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
151/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
152fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
153    let digits = sval(&vm.pop());
154    match digits.parse::<num_bigint::BigInt>() {
155        Ok(b) => with_host(|h| h.new_bigint(b)),
156        Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
157    }
158}
159
160/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
161/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
162/// Builds the `strings` array (carrying its `.raw` array) and calls
163/// `tag(strings, ...values)`.
164fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
165    let mut all = pop_n(vm, argc as usize);
166    let int_of = |v: &Value| match v {
167        Value::Int(n) => *n as usize,
168        Value::Float(f) => *f as usize,
169        _ => 0,
170    };
171    let tag = all.remove(0);
172    let n = int_of(&all.remove(0));
173    let mcount = int_of(&all.remove(0));
174    let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
175    let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
176    let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
177    // strings = cooked array; strings.raw = raw array (frozen in JS; nothing here
178    // mutates it).
179    let strings = with_host(|h| h.new_array(cooked));
180    let raw_arr = with_host(|h| h.new_array(raw));
181    // `GetTemplateObject` (13.2.8.4) defines `raw` as an own property that is
182    // neither writable, enumerable, nor configurable, then integrity-seals the
183    // template object. So `raw` stays out of `Object.keys(strings)` while
184    // `getOwnPropertyNames` still reports it.
185    with_host(|h| {
186        h.set_fn_prop(&strings, "raw", raw_arr);
187        h.set_prop_attrs(
188            &strings,
189            "raw",
190            host::PropAttrs {
191                writable: false,
192                enumerable: false,
193                configurable: false,
194            },
195        );
196    });
197    let mut call_args = vec![strings];
198    call_args.extend(values);
199    let r = host::invoke(&tag, call_args, None);
200    finish(vm, r)
201}
202
203/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
204/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
205/// iterator (each yielded value is awaited). Returns the iterator object/handle.
206fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
207    let src = vm.pop();
208    let r = host::get_async_iterator(&src);
209    finish(vm, r)
210}
211
212/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
213/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
214/// express (backreference/lookaround) throws a `SyntaxError` here.
215fn b_mkregex(vm: &mut VM, _: u8) -> Value {
216    let flags = sval(&vm.pop());
217    let pattern = sval(&vm.pop());
218    match crate::regexp::build_regexp(&pattern, &flags) {
219        Ok(v) => v,
220        Err(e) => abort(vm, e),
221    }
222}
223
224/// DAP per-statement marker (`node --dap` only; the compiler emits this before
225/// each statement under `debug`). Pops the source line pushed by the preceding
226/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
227/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
228/// session is active.
229fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
230    let line = match vm.pop() {
231        Value::Int(n) => n as u32,
232        _ => 0,
233    };
234    crate::dap::on_debug_line(line);
235    Value::Undef
236}
237
238/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
239/// or `member::SET`). Keeps the object on the stack.
240fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
241    let func = vm.pop();
242    let kind = match vm.pop() {
243        Value::Int(n) => n,
244        _ => 0,
245    };
246    let name = sval(&vm.pop());
247    let obj = vm.pop();
248    with_host(|h| {
249        if kind == host::member::SET {
250            h.set_accessor(&obj, &name, None, Some(func));
251        } else {
252            h.set_accessor(&obj, &name, Some(func), None);
253        }
254    });
255    obj
256}
257
258fn b_await(vm: &mut VM, _: u8) -> Value {
259    let v = vm.pop();
260    match host::await_value(v) {
261        Ok(r) => r,
262        Err(e) => abort(vm, e),
263    }
264}
265
266// ── classes / super / generators / property keys (compiler-emitted ops) ──────
267
268fn b_mkclass(vm: &mut VM, _: u8) -> Value {
269    let ctor = vm.pop();
270    let parent = vm.pop();
271    let name = sval(&vm.pop());
272    host::build_class(&name, parent, ctor)
273}
274
275fn b_def_member(vm: &mut VM, _: u8) -> Value {
276    let func = vm.pop();
277    let is_static = matches!(vm.pop(), Value::Bool(true));
278    let kind = match vm.pop() {
279        Value::Int(n) => n,
280        _ => 0,
281    };
282    let name = sval(&vm.pop());
283    let class_val = vm.pop();
284    host::define_member(&class_val, &name, kind, is_static, func);
285    class_val
286}
287
288fn b_def_field(vm: &mut VM, _: u8) -> Value {
289    // `name_anon`: the initializer was an anonymous function definition, so
290    // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
291    // decided by the compiler, not re-derived from the produced value.
292    let name_anon = matches!(vm.pop(), Value::Bool(true));
293    let thunk = vm.pop();
294    let name = sval(&vm.pop());
295    let class_val = vm.pop();
296    host::define_field(&class_val, &name, thunk, name_anon);
297    class_val
298}
299
300/// `super(...args)` in a derived constructor: run the parent constructor on the
301/// current `this`, then this class's field initializers.
302fn b_super_call(vm: &mut VM, argc: u8) -> Value {
303    let args = pop_n(vm, argc as usize);
304    let this = with_host(|h| h.current_this());
305    let this = match this {
306        Some(t) => t,
307        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
308    };
309    // The class whose constructor is running = the running method's home class.
310    let (parent, fields) = with_host(|h| h.super_context());
311    let (parent, fields) = match parent {
312        Some(p) => (p, fields),
313        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
314    };
315    let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
316    let r = host::super_construct(&parent, args, &this, &nt);
317    if let Err(e) = r {
318        return abort(vm, e);
319    }
320    // Run this (derived) class's own instance-field initializers after super.
321    for (name, thunk, name_anon) in fields {
322        if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
323            return abort(vm, e);
324        }
325    }
326    Value::Undef
327}
328
329/// `super.name` — a method from the parent's prototype, or a getter's result.
330fn b_super_get(vm: &mut VM, _: u8) -> Value {
331    let name = sval(&vm.pop());
332    match with_host(|h| h.super_resolve(&name)) {
333        host::SuperRef::Data(v) => v,
334        host::SuperRef::Getter(getter) => {
335            let this = with_host(|h| h.current_this());
336            match host::invoke(&getter, Vec::new(), this) {
337                Ok(v) => v,
338                Err(e) => abort(vm, e),
339            }
340        }
341    }
342}
343
344/// Close every loop iterator parked on `vm`'s stack at the op now executing,
345/// innermost first. Called where a chunk is about to be halted abruptly, since
346/// the code that would ordinarily close them is being jumped over.
347///
348/// A close runs user code (a generator's `finally`), which can itself throw; the
349/// error is deliberately dropped, because it must not replace the completion
350/// that caused the unwind.
351fn close_parked_iters(vm: &mut VM) {
352    let n = host::parked_iters(vm);
353    if n == 0 {
354        return;
355    }
356    // The completion that caused the unwind is already pending on the host.
357    // Closing an iterator resumes ANOTHER generator, which settles its own
358    // signal/error state, so the pending one is saved across the close and put
359    // back — otherwise the outer `.return()` would be lost.
360    let saved = with_host(|h| (h.signal.take(), h.error.take()));
361    for _ in 0..n {
362        let it = vm.pop();
363        let _ = close_iterator(&it);
364    }
365    with_host(|h| {
366        h.signal = saved.0;
367        h.error = saved.1;
368    });
369}
370
371fn b_yield(vm: &mut VM, _: u8) -> Value {
372    let v = vm.pop();
373    match host::gen_yield(v) {
374        Ok(sent) => {
375            // A `.return()`/`.throw()` injected on resume sets a pending Return
376            // signal (or error); halt the chunk so the body unwinds through any
377            // enclosing `try/finally`, exactly like a source `return`/`throw`.
378            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
379                // Halting jumps past the loop exits, so the `for…of` / `yield*`
380                // iterators parked on this chunk's stack would be abandoned
381                // still-suspended. They sit directly beneath the yielded value
382                // (innermost last), and the compiler recorded how many are
383                // there for this exact op.
384                close_parked_iters(vm);
385                vm.ip = vm.chunk.ops.len();
386            }
387            sent
388        }
389        // An injected `.throw()` comes back as an error rather than a signal,
390        // and abandons the parked iterators the same way. The thrown value is
391        // already on the host as `exc`; `close_parked_iters` puts back whatever
392        // it saves, so the close cannot swallow it.
393        Err(e) => {
394            close_parked_iters(vm);
395            abort(vm, e)
396        }
397    }
398}
399
400/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
401///
402/// It called `JsHost::property_key` directly, which is the primitive-only half
403/// of the conversion, so an object key never ran `ToPrimitive`:
404/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
405/// member form `a[o] = 1` — which does go through `host::to_property_key` —
406/// keyed on `"TS"`. The two forms are the same abstract operation and now share
407/// the same implementation.
408fn b_propkey(vm: &mut VM, _: u8) -> Value {
409    let v = vm.pop();
410    match host::to_property_key(&v) {
411        Ok(k) => with_host(|h| h.new_str(k)),
412        Err(e) => abort(vm, e),
413    }
414}
415
416fn b_new_target(_vm: &mut VM, _: u8) -> Value {
417    with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
418}
419
420/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
421/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
422/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
423/// `0/0 === NaN`, so `/` is lowered here instead.
424///
425/// Being a builtin rather than a native op means it does NOT reach the numeric
426/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
427/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
428/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
429/// hook now, so `/` coerces exactly as `*` and `-` do.
430fn b_div(vm: &mut VM, _: u8) -> Value {
431    let b = vm.pop();
432    let a = vm.pop();
433    let r = numeric_hook(NumOp::Div, &a, &b);
434    finish(vm, r)
435}
436
437/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
438/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
439/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
440/// one code path that already handles it.
441fn b_pow(vm: &mut VM, _: u8) -> Value {
442    let b = vm.pop();
443    let a = vm.pop();
444    let r = numeric_hook(NumOp::Pow, &a, &b);
445    finish(vm, r)
446}
447
448/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
449fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
450    let excluded = vm.pop();
451    let obj = vm.pop();
452    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
453        .unwrap_or_default()
454        .iter()
455        .map(|v| with_host(|h| h.str_of(v)))
456        .collect();
457    with_host(|h| {
458        let props: IndexMap<String, Value> = match h.get(&obj) {
459            Some(JsObj::Object(m)) => m
460                .iter()
461                .filter(|(k, _)| !excl.contains(k))
462                .map(|(k, v)| (k.clone(), v.clone()))
463                .collect(),
464            _ => IndexMap::new(),
465        };
466        h.new_object(props)
467    })
468}
469
470// ── helpers ──────────────────────────────────────────────────────────────────
471
472fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
473    let mut v = Vec::with_capacity(n);
474    for _ in 0..n {
475        v.push(vm.pop());
476    }
477    v.reverse();
478    v
479}
480
481/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
482fn sval(v: &Value) -> String {
483    if let Value::Str(s) = v {
484        return (**s).clone();
485    }
486    with_host(|h| h.as_str(v)).unwrap_or_default()
487}
488
489/// The same string, without `sval`'s deep copy. Every identifier the compiler
490/// emits is a `Value::Str` constant, so a variable read or write that went
491/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
492/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
493/// handle is a refcount bump instead.
494fn sname(v: &Value) -> std::sync::Arc<String> {
495    match v {
496        Value::Str(s) => s.clone(),
497        _ => std::sync::Arc::new(sval(v)),
498    }
499}
500
501fn abort(vm: &mut VM, e: String) -> Value {
502    with_host(|h| h.error = Some(e));
503    vm.ip = vm.chunk.ops.len();
504    Value::Undef
505}
506
507/// Halt the chunk if a call left an error or non-local signal pending.
508fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
509    match r {
510        Ok(v) => {
511            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
512                vm.ip = vm.chunk.ops.len();
513            }
514            v
515        }
516        Err(e) => abort(vm, e),
517    }
518}
519
520// ── name handlers ─────────────────────────────────────────────────────────────
521
522/// The value a bare global identifier resolves to, or `None` if unbound.
523///
524/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
525/// which must agree: a name reachable one way and not the other is exactly the
526/// discrepancy that left `globalThis.process` undefined while `process` worked.
527pub(crate) fn global_binding(name: &str) -> Option<Value> {
528    if let Some(v) = with_host(|h| h.read_name(name)) {
529        return Some(v);
530    }
531    // Globals bound lazily: numeric sentinels + builtin namespaces.
532    match name {
533        "undefined" => return Some(Value::Undef),
534        "NaN" => return Some(Value::Float(f64::NAN)),
535        "Infinity" => return Some(Value::Float(f64::INFINITY)),
536        // One object, not a fresh one per read: `globalThis === globalThis` is
537        // `true` in JS, and `globalThis.x = 1` is readable back as
538        // `globalThis.x`. Both were false while each read minted a new object.
539        // `global` is Node's alias for the same object.
540        "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
541        _ => {}
542    }
543    if is_namespace(name) || is_known_builtin(name) {
544        return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
545    }
546    None
547}
548
549fn b_getlocal(vm: &mut VM, _: u8) -> Value {
550    let name = sname(&vm.pop());
551    match global_binding(&name) {
552        Some(v) => v,
553        None => abort(vm, host::ref_error(&name)),
554    }
555}
556
557/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
558/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
559/// code — and, either way, never rebinds the name.
560const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
561
562fn readonly_global_error(name: &str) -> String {
563    host::type_error(&format!(
564        "Cannot assign to read only property '{name}' of object '#<Object>'"
565    ))
566}
567
568fn b_setlocal(vm: &mut VM, _: u8) -> Value {
569    let val = vm.pop();
570    let name = sname(&vm.pop());
571    // Sloppy assignment to a non-writable global is DISCARDED, not applied:
572    // `undefined = 1` used to rebind the name and make every later `undefined`
573    // read back as `1`.
574    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
575        return val;
576    }
577    // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
578    // immutable binding). This used to succeed silently.
579    if !with_host(|h| h.set_name(&name, val.clone())) {
580        return abort(vm, host::type_error("Assignment to constant variable."));
581    }
582    val
583}
584
585/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
586/// where sloppy code silently creates a global, strict code throws
587/// `ReferenceError: x is not defined`.
588///
589/// A separate opcode rather than a runtime flag: strictness is a static property
590/// of the code, so the compiler already knows which of the two an assignment is
591/// and sloppy code — everything in a CommonJS module without the directive —
592/// keeps the exact instruction it had.
593fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
594    let val = vm.pop();
595    let name = sname(&vm.pop());
596    if !binding_exists(&name) {
597        return abort(vm, host::ref_error(&name));
598    }
599    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
600        return abort(vm, readonly_global_error(&name));
601    }
602    if !with_host(|h| h.set_name(&name, val.clone())) {
603        return abort(vm, host::type_error("Assignment to constant variable."));
604    }
605    val
606}
607
608/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
609/// materialised builtin namespace. `global_binding` answers the same question
610/// but ALLOCATES the namespace object to do it, which an assignment then throws
611/// away.
612fn binding_exists(name: &str) -> bool {
613    if with_host(|h| h.has_name(name)) {
614        return true;
615    }
616    matches!(
617        name,
618        "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
619    ) || is_namespace(name)
620        || is_known_builtin(name)
621}
622
623fn b_declare(vm: &mut VM, _: u8) -> Value {
624    let val = vm.pop();
625    let name = sname(&vm.pop());
626    with_host(|h| h.declare_name(&name, val.clone()));
627    val
628}
629
630/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
631/// assignment to the name throws instead of overwriting it.
632fn b_declare_const(vm: &mut VM, _: u8) -> Value {
633    let val = vm.pop();
634    let name = sname(&vm.pop());
635    with_host(|h| h.declare_const_name(&name, val.clone()));
636    val
637}
638
639/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
640/// open block scopes, so the name outlives the block it was written in.
641fn b_declare_var(vm: &mut VM, _: u8) -> Value {
642    let val = vm.pop();
643    let name = sname(&vm.pop());
644    with_host(|h| h.declare_var_name(&name, val.clone()));
645    val
646}
647
648fn b_push_scope(_: &mut VM, _: u8) -> Value {
649    with_host(|h| h.push_scope());
650    Value::Undef
651}
652
653fn b_pop_scope(_: &mut VM, _: u8) -> Value {
654    with_host(|h| h.pop_scope());
655    Value::Undef
656}
657
658fn b_copy_scope(_: &mut VM, _: u8) -> Value {
659    with_host(|h| h.copy_scope());
660    Value::Undef
661}
662
663fn b_delname(vm: &mut VM, _: u8) -> Value {
664    let name = sval(&vm.pop());
665    with_host(|h| h.del_name(&name));
666    Value::Bool(true)
667}
668
669fn b_this(_vm: &mut VM, _: u8) -> Value {
670    with_host(|h| h.current_this().unwrap_or(Value::Undef))
671}
672
673fn b_load_null(_vm: &mut VM, _: u8) -> Value {
674    with_host(|h| h.null())
675}
676
677// ── attribute / item handlers ─────────────────────────────────────────────────
678
679fn b_getattr(vm: &mut VM, _: u8) -> Value {
680    let name = sval(&vm.pop());
681    let recv = vm.pop();
682    match get_property(&recv, &name) {
683        Ok(v) => v,
684        Err(e) => abort(vm, e),
685    }
686}
687
688/// Read `recv.name` (also the computed-key path for string keys). Walks own
689/// properties, accessors, and the prototype chain (class methods / getters).
690/// Read one small piece out of `recv`'s heap cell under a short borrow.
691///
692/// The closure must not call back into the host (`with_host` is a `RefCell`
693/// borrow and re-entering panics) — which is exactly why it hands back only the
694/// value needed: the caller re-enters freely afterwards. This replaces the old
695/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
696/// `String` just to look at it.
697fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
698    with_host(|h| h.get(recv).and_then(f))
699}
700
701/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
702/// reaches it without a closer link already owning `name`.
703///
704/// A proxy prototype answers only from the position it occupies in the chain: a
705/// nearer prototype that owns the key (as a data property or an accessor) still
706/// wins, exactly as `OrdinaryGet` walks one link at a time.
707pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
708    with_host(|h| {
709        let mut cur = h.proto_of(recv);
710        for _ in 0..100 {
711            let p = cur?;
712            match h.get(&p) {
713                Some(JsObj::Proxy { .. }) => return Some(p),
714                Some(JsObj::Object(props)) if props.contains_key(name) => return None,
715                _ => {}
716            }
717            if h.own_accessor(&p, name).is_some() {
718                return None;
719            }
720            cur = h.proto_of(&p);
721        }
722        None
723    })
724}
725
726pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
727    // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
728    // when the receiver carries no such private element — it does NOT read back
729    // as `undefined`, which is what `C.prototype.method.call({})` used to do.
730    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
731        return Err(private_brand_message(name, false));
732    }
733    get_property_recv(recv, name, recv)
734}
735
736/// The `TypeError` a failed private brand check raises. Node words it two ways:
737/// a private METHOD or accessor names the class the receiver should have been an
738/// instance of, while a private FIELD names the member.
739pub fn private_brand_message(name: &str, writing: bool) -> String {
740    if with_host(|h| h.is_private_method(name)) {
741        if let Some(class) = with_host(|h| h.current_home_class_name()) {
742            return host::type_error(&format!("Receiver must be an instance of class {class}"));
743        }
744    }
745    let verb = if writing { "write" } else { "read" };
746    let prep = if writing { "to" } else { "from" };
747    host::type_error(&format!(
748        "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
749    ))
750}
751
752/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
753/// from and is what a getter sees as `this`; it differs from `recv` only when the
754/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
755/// and a Proxy `get` trap's third argument both need it. Every ordinary read
756/// passes `recv` itself.
757pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
758    // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
759    // target. Checked before anything else so no ordinary-object shortcut can
760    // read past the handler.
761    if let Some(v) = crate::proxy::get(recv, name, receiver)? {
762        return Ok(v);
763    }
764    if with_host(|h| h.is_nullish(recv)) {
765        return Err(host::type_error(&format!(
766            "Cannot read properties of {} (reading '{name}')",
767            with_host(|h| h.str_of(recv))
768        )));
769    }
770    // A read off `globalThis` for a name the object does not own falls back to
771    // the same lazy global binding the bare identifier gets. Without it the
772    // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
773    // and `.JSON` were all `undefined`, so `process === globalThis.process` was
774    // `false` and any `globalThis.X` feature probe reported the feature missing.
775    if with_host(|h| h.is_global_object(recv)) {
776        let own = with_host(|h| match h.get(recv) {
777            Some(JsObj::Object(p)) => p.contains_key(name),
778            _ => false,
779        });
780        // The CommonJS wrapper's parameters are function locals in Node, not
781        // global-object properties: `typeof globalThis.require` is `undefined`
782        // there even though the bare `require` works.
783        const CJS_WRAPPER_LOCALS: &[&str] = &[
784            "require",
785            "module",
786            "exports",
787            "__filename",
788            "__dirname",
789            "__cjs_require",
790            "__cjs_resolve",
791        ];
792        if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
793            if let Some(v) = global_binding(name) {
794                return Ok(v);
795            }
796        }
797    }
798    // Accessor (own or inherited getter) takes precedence over the chain walk.
799    // The getter runs with the RECEIVER as `this`, not the object that owns it.
800    if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
801        return match getter {
802            Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
803            None => Ok(Value::Undef), // set-only property reads as undefined
804        };
805    }
806    // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
807    // one expose it to a plain read, not just to `Object.prototype.toString` —
808    // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
809    // inherits `'Uint8Array'` from the typed-array prototype it now really has.
810    // Anything the receiver's own chain provides wins (a class may define its
811    // own getter), so this is only the fallback.
812    if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
813        if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
814            return Ok(with_host(|h| h.new_str(tag)));
815        }
816    }
817    // `constructor`: a user class/function sets it on the prototype chain, and
818    // that wins; otherwise every builtin instance reports its native
819    // constructor (so `[].constructor`, `new Map().constructor`,
820    // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
821    if name == "constructor" {
822        if let Some(v) = with_host(|h| {
823            match h.get(recv) {
824                Some(JsObj::Object(p)) => p.get("constructor").cloned(),
825                _ => None,
826            }
827            .or_else(|| host::lookup_chain(h, recv, "constructor"))
828        }) {
829            return Ok(v);
830        }
831        if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
832            return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
833        }
834    }
835    // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
836    // answers for EVERY object that inherits from it, not only plain ones —
837    // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
838    // so an array, function or builtin instance read `undefined`. An object with
839    // a null prototype inherits no such accessor and reads `undefined`, which is
840    // why this is skipped there rather than answering `null`.
841    if name == "__proto__"
842        && !with_host(|h| h.has_null_proto(recv))
843        && peek(recv, |o| match o {
844            JsObj::Object(p) => Some(p.contains_key("__proto__")),
845            _ => Some(false),
846        }) != Some(true)
847    {
848        return Ok(prototype_of(recv));
849    }
850    let kind = with_host(|h| h.kind_of(recv));
851    Ok(match kind {
852        Some(ObjKind::Object) => {
853            let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
854            // Typed-array element read (`ta[i]`): elements live in a hidden
855            // `@@elems`, not as own numeric props, so intercept integer keys.
856            if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
857                if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
858                    return Ok(v);
859                }
860            }
861            // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
862            // own numeric props, so integer keys read through to it.
863            if numeric
864                && peek(recv, |o| match o {
865                    JsObj::Object(p) => Some(p.contains_key("@@bytes")),
866                    _ => None,
867                })
868                .unwrap_or(false)
869            {
870                return Ok(crate::stdlib::buffer::byte_get(recv, name));
871            }
872            if let Some(v) = peek(recv, |o| match o {
873                JsObj::Object(p) => p.get(name).cloned(),
874                _ => None,
875            }) {
876                v
877            } else if let Some(link) = proxy_proto_link(recv, name) {
878                // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
879                // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
880                // receiver, so the trap sees the child as `receiver` and `this`
881                // inside a trap-served getter resolves to the child, not the
882                // proxy. `lookup_chain` cannot do this: it reads property maps,
883                // and a proxy has none.
884                return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
885            } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
886                // A method / data property inherited from the prototype chain.
887                v
888            } else if crate::stdlib::native_tag(recv)
889                .map(|tag| crate::stdlib::instance_has_method(&tag, name))
890                .unwrap_or(false)
891            {
892                // A native instance method read as a property (`server.listen`) →
893                // a bound method, dispatched via `instance_call` when invoked.
894                bound_method(recv, name)
895            } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
896                // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
897                // read as `undefined` there — which is also what makes
898                // `Object.create(null) + 1` the spec `TypeError` instead of a
899                // silent `"[object Object]1"`.
900                bound_method(recv, name)
901            } else {
902                Value::Undef
903            }
904        }
905        Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
906            function_property(recv, name)
907        }
908        Some(ObjKind::Symbol) => match name {
909            "description" => {
910                match peek(recv, |o| match o {
911                    JsObj::Symbol { desc, .. } => desc.clone(),
912                    _ => None,
913                }) {
914                    Some(d) => with_host(|h| h.new_str(d)),
915                    None => Value::Undef,
916                }
917            }
918            "toString" => bound_method(recv, name),
919            _ => Value::Undef,
920        },
921        Some(ObjKind::BigInt) => {
922            if matches!(
923                name,
924                "toString" | "valueOf" | "toLocaleString" | "constructor"
925            ) {
926                bound_method(recv, name)
927            } else {
928                Value::Undef
929            }
930        }
931        Some(ObjKind::RegExp) => {
932            // A RegExp holds no collection, so cloning the compiled pattern here
933            // does not scale with any input size; `regexp_property` re-enters the
934            // host to allocate `source`/`flags`, so it cannot run under a borrow.
935            let r = peek(recv, |o| match o {
936                JsObj::RegExp(r) => Some(r.clone()),
937                _ => None,
938            });
939            match r {
940                Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
941                    if crate::regexp::is_regexp_method(name) {
942                        bound_method(recv, name)
943                    } else {
944                        Value::Undef
945                    }
946                }),
947                None => Value::Undef,
948            }
949        }
950        // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
951        // the read must be `undefined` rather than a live count.
952        Some(ObjKind::Map) => {
953            let (len, weak) = peek(recv, |o| match o {
954                JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
955                _ => None,
956            })
957            .unwrap_or((0, false));
958            match name {
959                "size" if !weak => Value::Float(len as f64),
960                "@@iterator" => bound_method(recv, name),
961                _ if is_map_method(name) => bound_method(recv, name),
962                _ => Value::Undef,
963            }
964        }
965        Some(ObjKind::Set) => {
966            let (len, weak) = peek(recv, |o| match o {
967                JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
968                _ => None,
969            })
970            .unwrap_or((0, false));
971            match name {
972                "size" if !weak => Value::Float(len as f64),
973                "@@iterator" => bound_method(recv, name),
974                _ if is_set_method(name) => bound_method(recv, name),
975                _ => Value::Undef,
976            }
977        }
978        Some(ObjKind::Generator) => {
979            if is_generator_method(name) {
980                bound_method(recv, name)
981            } else {
982                Value::Undef
983            }
984        }
985        Some(ObjKind::Promise) => {
986            if matches!(name, "then" | "catch" | "finally") {
987                bound_method(recv, name)
988            } else {
989                Value::Undef
990            }
991        }
992        Some(ObjKind::Iter) => {
993            if matches!(name, "next" | "return" | "@@iterator") {
994                bound_method(recv, name)
995            } else {
996                Value::Undef
997            }
998        }
999        Some(ObjKind::Array) => {
1000            if name == "length" {
1001                let n = peek(recv, |o| match o {
1002                    JsObj::Array(items) => Some(items.len()),
1003                    _ => None,
1004                })
1005                .unwrap_or(0);
1006                Value::Float(n as f64)
1007            } else if let Ok(i) = name.parse::<usize>() {
1008                peek(recv, |o| match o {
1009                    JsObj::Array(items) => items.get(i).cloned(),
1010                    _ => None,
1011                })
1012                .unwrap_or(Value::Undef)
1013            } else if name == "@@iterator" || is_array_method(name) || is_object_method(name) {
1014                bound_method(recv, name)
1015            } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1016                // Extra own props attached to an array (e.g. `RegExp.exec` result's
1017                // `.index`/`.input`/`.groups`).
1018                v
1019            } else {
1020                Value::Undef
1021            }
1022        }
1023        Some(ObjKind::Str) => {
1024            // `.length` and `s[i]` count UTF-16 code units, not code points.
1025            if name == "length" {
1026                let n = peek(recv, |o| match o {
1027                    JsObj::Str(s) => Some(crate::utf16::len(s)),
1028                    _ => None,
1029                })
1030                .unwrap_or(0);
1031                Value::Float(n as f64)
1032            } else if let Ok(i) = name.parse::<usize>() {
1033                match peek(recv, |o| match o {
1034                    JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1035                    _ => None,
1036                }) {
1037                    Some(c) => with_host(|h| h.new_str(c)),
1038                    None => Value::Undef,
1039                }
1040            } else if name == "@@iterator" || is_string_method(name) {
1041                bound_method(recv, name)
1042            } else {
1043                Value::Undef
1044            }
1045        }
1046        Some(ObjKind::Builtin) => {
1047            let ns = peek(recv, |o| match o {
1048                JsObj::Builtin(ns) => Some(ns.clone()),
1049                _ => None,
1050            })
1051            .unwrap_or_default();
1052            namespace_property(&ns, name)
1053        }
1054        _ => {
1055            // Primitive numbers/booleans: method access -> bound method.
1056            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1057                bound_method(recv, name)
1058            } else {
1059                Value::Undef
1060            }
1061        }
1062    })
1063}
1064
1065/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1066/// object literal because the module cache is the single source of truth: a
1067/// populated copy would answer reads correctly and silently ignore a `delete`,
1068/// which is the operation the property exists for.
1069pub const REQUIRE_CACHE: &str = "__cjs_cache";
1070
1071/// The builtin constructor name for a value with no own/inherited `constructor`
1072/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1073/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1074/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1075/// whose `.constructor.name` is `""` in Node — not worth modelling).
1076fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1077    match h.get(recv) {
1078        Some(JsObj::Array(_)) => Some("Array"),
1079        Some(JsObj::Object(props)) => {
1080            // A native instance reports its own constructor, not Object — e.g.
1081            // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1082            // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1083            // the `@@native` tag off the already-borrowed host (calling
1084            // `native_tag`, which re-enters `with_host`, would double-borrow).
1085            match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1086                Some("Buffer") => Some("Buffer"),
1087                Some("URL") => Some("URL"),
1088                Some("Date") => Some("Date"),
1089                Some("WeakRef") => Some("WeakRef"),
1090                Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1091                Some("TextEncoder") => Some("TextEncoder"),
1092                Some("TextDecoder") => Some("TextDecoder"),
1093                Some("EventEmitter") => Some("EventEmitter"),
1094                Some("Timeout") => Some("Timeout"),
1095                Some("Immediate") => Some("Immediate"),
1096                _ => Some("Object"),
1097            }
1098        }
1099        Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1100        Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1101        Some(JsObj::Promise { .. }) => Some("Promise"),
1102        Some(JsObj::Str(_)) => Some("String"),
1103        Some(JsObj::Symbol { .. }) => Some("Symbol"),
1104        Some(JsObj::BigInt(_)) => Some("BigInt"),
1105        Some(JsObj::RegExp(_)) => Some("RegExp"),
1106        Some(JsObj::Iter { .. }) => Some("Iterator"),
1107        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
1108            Some("Function")
1109        }
1110        _ => match recv {
1111            Value::Float(_) | Value::Int(_) => Some("Number"),
1112            Value::Bool(_) => Some("Boolean"),
1113            _ => None,
1114        },
1115    }
1116}
1117
1118/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1119/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1120/// `process`), whose `.name` is `undefined` in Node.
1121///
1122/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1123/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1124/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1125fn is_builtin_ctor(name: &str) -> bool {
1126    matches!(
1127        name,
1128        "Array"
1129            | "Object"
1130            | "Number"
1131            | "String"
1132            | "Boolean"
1133            | "Symbol"
1134            | "Function"
1135            | "Map"
1136            | "Set"
1137            | "WeakMap"
1138            | "WeakSet"
1139            | "Promise"
1140            | "BigInt"
1141            | "Iterator"
1142            | "RegExp"
1143            | "Date"
1144            | "ArrayBuffer"
1145            | "Uint8Array"
1146            | "Int8Array"
1147            | "Uint8ClampedArray"
1148            | "Int16Array"
1149            | "Uint16Array"
1150            | "Int32Array"
1151            | "Uint32Array"
1152            | "Float32Array"
1153            | "Float64Array"
1154            | "BigInt64Array"
1155            | "BigUint64Array"
1156            | "WeakRef"
1157            | "FinalizationRegistry"
1158            | "TextEncoder"
1159            | "TextDecoder"
1160            | "IncomingMessage"
1161            | "ServerResponse"
1162            | "EventEmitter"
1163            | "Buffer"
1164            | "URL"
1165            | "URLSearchParams"
1166            | "Timeout"
1167            | "Immediate"
1168    ) || host::ERROR_NAMES.contains(&name)
1169}
1170
1171fn bound_method(recv: &Value, name: &str) -> Value {
1172    with_host(|h| {
1173        h.alloc(JsObj::BoundMethod {
1174            recv: recv.clone(),
1175            name: name.to_string(),
1176        })
1177    })
1178}
1179
1180/// `Object.prototype` methods reachable on any object.
1181fn is_object_method(name: &str) -> bool {
1182    matches!(
1183        name,
1184        "hasOwnProperty"
1185            | "isPrototypeOf"
1186            | "propertyIsEnumerable"
1187            | "toString"
1188            | "toLocaleString"
1189            | "valueOf"
1190            | "constructor"
1191    )
1192}
1193
1194/// The `Object.prototype` methods installed as thunks on the real
1195/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1196/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1197pub const OBJECT_PROTO_METHODS: &[&str] = &[
1198    "hasOwnProperty",
1199    "isPrototypeOf",
1200    "propertyIsEnumerable",
1201    "toString",
1202    "toLocaleString",
1203    "valueOf",
1204];
1205
1206pub fn is_object_builtin_method(name: &str) -> bool {
1207    matches!(
1208        name,
1209        "hasOwnProperty"
1210            | "isPrototypeOf"
1211            | "propertyIsEnumerable"
1212            | "toString"
1213            | "toLocaleString"
1214            | "valueOf"
1215    )
1216}
1217
1218/// Dispatch an `Object.prototype` builtin method on an object/instance.
1219pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1220    match name {
1221        "hasOwnProperty" => {
1222            let k = with_host(|h| h.property_key(&arg0(&args)));
1223            // A builtin namespace/prototype receiver (`Map.prototype`) reports
1224            // ownership via `has_property` (its methods resolve as thunks).
1225            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
1226                return Ok(Value::Bool(has_property(recv, &k)?));
1227            }
1228            // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
1229            // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
1230            // the target's property map.
1231            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
1232                let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
1233                return Ok(Value::Bool(!matches!(d, Value::Undef)));
1234            }
1235            // A Buffer's / typed array's own keys are its element indices: the
1236            // `length`/`byteLength` slots are internal bookkeeping, and V8
1237            // reports `hasOwnProperty('length')` as false for a typed array.
1238            // Shared with the `in` operator so the two cannot drift apart.
1239            if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
1240                return Ok(Value::Bool(hit));
1241            }
1242            let has = with_host(|h| match h.get(recv) {
1243                Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
1244                Some(JsObj::Array(items)) => {
1245                    k == "length"
1246                        || k.parse::<usize>()
1247                            .map(|i| i < items.len() && !h.is_hole(recv, i))
1248                            .unwrap_or(false)
1249                }
1250                _ => false,
1251            });
1252            Ok(Value::Bool(has))
1253        }
1254        "isPrototypeOf" => {
1255            let target = arg0(&args);
1256            // The ARGUMENT is what gets walked, so a proxy there needs its
1257            // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
1258            // proxy does not hold, which reported `false` for every proxy. From
1259            // the second hop on the chain is ordinary objects again, walked by
1260            // the recorded link exactly as before.
1261            let mut cur = match crate::proxy::get_prototype_of(&target)? {
1262                Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
1263                None => with_host(|h| h.proto_of(&target)),
1264            };
1265            while let Some(p) = cur {
1266                if with_host(|h| h.strict_eq(&p, recv)) {
1267                    return Ok(Value::Bool(true));
1268                }
1269                cur = with_host(|h| h.proto_of(&p));
1270            }
1271            Ok(Value::Bool(false))
1272        }
1273        "propertyIsEnumerable" => {
1274            let k = with_host(|h| h.str_of(&arg0(&args)));
1275            // Own *and* enumerable — a non-enumerable own slot reads false. On a
1276            // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
1277            // trap, since there is no property map to enumerate.
1278            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
1279                let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
1280                return Ok(Value::Bool(has));
1281            }
1282            let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
1283            Ok(Value::Bool(has))
1284        }
1285        "toString" => Ok(with_host(|h| {
1286            // An instance with a custom `toString` up the chain is handled by
1287            // call_method before reaching here; this is the default.
1288            let s = h.str_of(recv);
1289            h.new_str(s)
1290        })),
1291        // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
1292        // `Invoke(this, "toString")` — no locale behavior of its own. It was
1293        // installed as a thunk on `Object.prototype` but had no dispatch arm, so
1294        // calling it threw `is not a function` on every plain object.
1295        "toLocaleString" => {
1296            let v = host::call_method(recv, "toString", Vec::new())?;
1297            Ok(v)
1298        }
1299        "valueOf" => Ok(recv.clone()),
1300        _ => Err(host::type_error(&format!("{name} is not a function"))),
1301    }
1302}
1303
1304/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
1305/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
1306/// these (so the caller can try statics).
1307pub fn function_builtin_method(
1308    recv: &Value,
1309    name: &str,
1310    args: &[Value],
1311) -> Result<Option<Value>, String> {
1312    match name {
1313        "call" => {
1314            let this = args.first().cloned();
1315            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
1316            Ok(Some(host::invoke(recv, rest, this)?))
1317        }
1318        "apply" => {
1319            let this = args.first().cloned();
1320            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
1321            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
1322                Vec::new()
1323            } else {
1324                with_host(|h| h.iter_vec(&arr)).unwrap_or_default()
1325            };
1326            Ok(Some(host::invoke(recv, call_args, this)?))
1327        }
1328        "bind" => {
1329            let this = args.first().cloned().unwrap_or(Value::Undef);
1330            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
1331            Ok(Some(with_host(|h| {
1332                h.alloc(JsObj::BoundFunc {
1333                    target: recv.clone(),
1334                    this,
1335                    args: pre,
1336                })
1337            })))
1338        }
1339        "toString" => Ok(Some(with_host(|h| {
1340            let s = h.str_of(recv);
1341            h.new_str(s)
1342        }))),
1343        _ => Ok(None),
1344    }
1345}
1346
1347fn is_function_method(name: &str) -> bool {
1348    matches!(name, "call" | "apply" | "bind" | "toString")
1349}
1350fn is_map_method(name: &str) -> bool {
1351    matches!(
1352        name,
1353        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
1354    )
1355}
1356fn is_set_method(name: &str) -> bool {
1357    matches!(
1358        name,
1359        "add" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
1360    )
1361}
1362fn is_generator_method(name: &str) -> bool {
1363    matches!(name, "next" | "return" | "throw")
1364}
1365
1366/// A property read on a function/class value: own fn-props (statics, name,
1367/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
1368fn function_property(recv: &Value, name: &str) -> Value {
1369    // A class static, inherited down the constructor chain.
1370    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
1371        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
1372            return v;
1373        }
1374        // The chain may bottom out in a BUILTIN constructor (`class D extends
1375        // Array {}`), whose statics `class_static` cannot see — it only walks
1376        // `ClassVal.parent` links between user classes. Finish the lookup with an
1377        // ordinary read on that ancestor so `D.from` inherits `Array.from`.
1378        if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
1379            if let Ok(v) = get_property(&anc, name) {
1380                if !matches!(v, Value::Undef) {
1381                    return v;
1382                }
1383            }
1384        }
1385    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1386        return v;
1387    }
1388    // A method inherited via the function's [[Prototype]] chain (set with
1389    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
1390    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
1391    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1392        return v;
1393    }
1394    match name {
1395        "name" => with_host(|h| {
1396            let n = h.callable_name(recv);
1397            h.new_str(n)
1398        }),
1399        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
1400        "prototype" => ensure_fn_prototype(recv),
1401        _ if is_function_method(name) => bound_method(recv, name),
1402        _ => Value::Undef,
1403    }
1404}
1405
1406/// The `.prototype` of a function value, auto-created on first access (as Node
1407/// does for every non-arrow function) with `.constructor` linking back. Arrow
1408/// functions have no `prototype`.
1409fn ensure_fn_prototype(recv: &Value) -> Value {
1410    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
1411        return p;
1412    }
1413    // Only a constructor gets one: an arrow, a method definition and an async
1414    // function are not constructors, and a class sets its own (10.2.5).
1415    if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
1416        return Value::Undef;
1417    }
1418    if !with_host(|h| h.owns_prototype(recv)) {
1419        return Value::Undef;
1420    }
1421    with_host(|h| {
1422        let proto = h.new_object(IndexMap::new());
1423        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
1424            p.insert("constructor".to_string(), recv.clone());
1425        }
1426        h.hide_prop(&proto, "constructor");
1427        h.set_fn_prop(recv, "prototype", proto.clone());
1428        proto
1429    })
1430}
1431
1432/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
1433/// `console.log`).
1434pub fn namespace_property(ns: &str, name: &str) -> Value {
1435    // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
1436    // read sees whatever is loaded now and `delete` (see `delete_property`)
1437    // actually invalidates.
1438    if ns == REQUIRE_CACHE {
1439        return crate::module::cache_get(name).unwrap_or(Value::Undef);
1440    }
1441    // The ENTRY script's `require` is this builtin rather than the per-module
1442    // closure, so its `cache` has to be handed out here too.
1443    if ns == "require" && name == "cache" {
1444        return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
1445    }
1446    // Numeric constants.
1447    let konst = match (ns, name) {
1448        ("Math", "PI") => Some(std::f64::consts::PI),
1449        ("Math", "E") => Some(std::f64::consts::E),
1450        ("Math", "LN2") => Some(std::f64::consts::LN_2),
1451        ("Math", "LN10") => Some(std::f64::consts::LN_10),
1452        ("Math", "LOG2E") => Some(std::f64::consts::LOG2_E),
1453        ("Math", "LOG10E") => Some(std::f64::consts::LOG10_E),
1454        ("Math", "SQRT2") => Some(std::f64::consts::SQRT_2),
1455        ("Math", "SQRT1_2") => Some(std::f64::consts::FRAC_1_SQRT_2),
1456        ("Number", "MAX_SAFE_INTEGER") => Some(9007199254740991.0),
1457        ("Number", "MIN_SAFE_INTEGER") => Some(-9007199254740991.0),
1458        ("Number", "MAX_VALUE") => Some(f64::MAX),
1459        // The smallest positive value a Number can hold, which is the smallest
1460        // SUBNORMAL double (`5e-324`), not Rust's `f64::MIN_POSITIVE` — that is
1461        // the smallest *normal* double, `2.2250738585072014e-308`, ~256 binary
1462        // orders of magnitude too large.
1463        ("Number", "MIN_VALUE") => Some(f64::from_bits(1)),
1464        ("Number", "EPSILON") => Some(f64::EPSILON),
1465        ("Number", "POSITIVE_INFINITY") => Some(f64::INFINITY),
1466        ("Number", "NEGATIVE_INFINITY") => Some(f64::NEG_INFINITY),
1467        ("Number", "NaN") => Some(f64::NAN),
1468        _ => None,
1469    };
1470    if let Some(k) = konst {
1471        return Value::Float(k);
1472    }
1473    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
1474    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
1475    // `undefined`.
1476    if name == "name" && is_builtin_ctor(ns) {
1477        return with_host(|h| h.new_str(ns.to_string()));
1478    }
1479    // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
1480    // computed property/method key.
1481    if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
1482        return with_host(|h| h.well_known_symbol(name));
1483    }
1484    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
1485    // `buffer.Buffer`, `url.URL`).
1486    if let Some(v) = crate::stdlib::constant(ns, name) {
1487        return v;
1488    }
1489    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
1490    // `Array.prototype`, …): a prototype namespace whose methods are callable
1491    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
1492    // `get-intrinsic`/`function-bind` family).
1493    if name == "prototype" && is_builtin_ctor(ns) {
1494        // Same reasoning as the native prototypes below, for the error
1495        // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
1496        // `error_protos` object, so `Error.prototype` has to read back that same
1497        // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
1498        // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
1499        // Error("x")) === Error.prototype` was false, and `typeof
1500        // Error.prototype` was `"function"` where node says `"object"`.
1501        if host::ERROR_NAMES.contains(&ns) {
1502            if let Some(p) = with_host(|h| {
1503                h.ensure_error_protos();
1504                host::error_proto_of(h, ns)
1505            }) {
1506                return p;
1507            }
1508        }
1509        // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
1510        // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
1511        // Buffer.prototype` must compare equal, which a freshly-allocated
1512        // `Builtin` handle never can.
1513        if let Some(p) = with_host(|h| {
1514            h.ensure_native_protos();
1515            h.native_proto(ns)
1516        }) {
1517            return p;
1518        }
1519        let _ = ns;
1520        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
1521    }
1522    // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
1523    // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
1524    // above never fired and the read produced `undefined` — which broke the ES5
1525    // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
1526    // reads `StringDecoder.prototype.end` at load, and threw
1527    // `Cannot read properties of undefined (reading 'end')`. Built from the same
1528    // instance-method table a method read consults, so the two cannot disagree.
1529    if name == "prototype" {
1530        if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
1531            return p;
1532        }
1533    }
1534    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
1535    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
1536    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
1537    if let Some(ctor) = ns.strip_suffix(".prototype") {
1538        return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
1539    }
1540    let qualified = format!("{ns}.{name}");
1541    if is_known_builtin(&qualified) {
1542        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
1543    }
1544    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
1545    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
1546        return v;
1547    }
1548    Value::Undef
1549}
1550
1551/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
1552/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
1553/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
1554/// libraries type-check on; every other method routes through normal method
1555/// dispatch on `recv`.
1556pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
1557    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
1558    // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
1559    // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
1560    if ctor == "Error" && method == "toString" {
1561        let s = with_host(|h| h.error_to_string(recv)).unwrap_or_else(|| {
1562            with_host(|h| {
1563                let name = host::lookup_chain(h, recv, "name")
1564                    .map(|n| h.str_of(&n))
1565                    .unwrap_or_else(|| "Error".into());
1566                let msg = host::lookup_chain(h, recv, "message")
1567                    .map(|m| h.str_of(&m))
1568                    .unwrap_or_default();
1569                if msg.is_empty() {
1570                    name
1571                } else {
1572                    format!("{name}: {msg}")
1573                }
1574            })
1575        });
1576        return Ok(with_host(|h| h.new_str(s)));
1577    }
1578    if ctor == "Object" && method == "toString" {
1579        // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
1580        // (own or inherited, data property or getter) replaces the builtin brand,
1581        // which is how a class advertises its own (`class C { get
1582        // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
1583        // runs outside the host borrow so an accessor can be invoked.
1584        // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
1585        // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
1586        // the ordinary receiver does, to keep the read off objects that have no
1587        // tag) would always miss and brand every tagged proxy `[object Object]`.
1588        let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1589            || with_host(|h| {
1590                host::lookup_chain(h, recv, "@@toStringTag").is_some()
1591                    || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1592            });
1593        if tagged {
1594            let t = get_property(recv, "@@toStringTag")?;
1595            if let Some(s) = with_host(|h| h.as_str(&t)) {
1596                return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
1597            }
1598        }
1599        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
1600    }
1601    // These thunks now live on the real `Object.prototype` object, i.e. on the
1602    // receiver's own chain — routing back through `call_method` would re-resolve
1603    // this very thunk and recurse.
1604    if ctor == "Object" && is_object_builtin_method(method) {
1605        return object_builtin_method(recv, method, args);
1606    }
1607    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
1608    // emitter method directly against `recv` (routing back through `call_method`
1609    // would re-resolve the mixed-in thunk and recurse).
1610    if ctor == "EventEmitter" {
1611        return crate::stdlib::events::instance_call(recv, method, args);
1612    }
1613    // Same recursion hazard for the exotics with a real prototype object: the
1614    // thunk now lives ON the receiver's prototype chain, so `call_method` would
1615    // re-resolve this very thunk. Dispatch straight to the native instance
1616    // implementation when the receiver is in fact an instance of `ctor`.
1617    if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
1618        return crate::stdlib::buffer::instance_call(recv, method, &args);
1619    }
1620    // The shared typed-array methods now live on the `%TypedArray%.prototype`
1621    // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
1622    // appears for anything read directly off `Uint8Array.prototype`. Both
1623    // dispatch the same way, and both must bypass `call_method` or the thunk
1624    // would re-resolve itself off the receiver's chain and recurse.
1625    if ctor == "Uint8Array" || ctor == "TypedArray" {
1626        match crate::stdlib::native_tag(recv).as_deref() {
1627            Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
1628            Some("TypedArray") => {
1629                return crate::stdlib::typedarray::instance_call(recv, method, &args)
1630            }
1631            _ => {}
1632        }
1633    }
1634    // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
1635    // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
1636    // `LengthOfArrayLike`), which is what makes
1637    // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
1638    // is not an Array, so `call_method` would report the method missing.
1639    if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
1640        return array_generic(recv, method, args);
1641    }
1642    // The general form of the two special cases above: a thunk taken off a native
1643    // constructor's real prototype, invoked with a receiver that IS an instance of
1644    // that constructor. Routing back through `call_method` would re-resolve this
1645    // very thunk off the receiver's own chain and recurse forever, which is why
1646    // each such prototype needed a hand-written bypass; now they all have one.
1647    if crate::stdlib::native_tag(recv).as_deref() == Some(ctor) {
1648        return crate::stdlib::instance_call(ctor, recv, method, args);
1649    }
1650    host::call_method(recv, method, args)
1651}
1652
1653/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
1654/// one, or `None` when reading that symbol must yield `undefined`.
1655///
1656/// Every builtin brand is already computed in exactly one place (`object_tag`),
1657/// so this reuses it and subtracts the legacy builtins, which brand for
1658/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
1659/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
1660/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
1661/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
1662/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
1663/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
1664/// `TextEncoder`/`TextDecoder` all read their brand.
1665fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
1666    // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
1667    // of which `object_tag` already brands.
1668    let tag = object_brand(h, v);
1669    const NO_TAG: &[&str] = &[
1670        "Undefined",
1671        "Null",
1672        "Boolean",
1673        "Number",
1674        "String",
1675        "Array",
1676        "Function",
1677        "Object",
1678        "Date",
1679        "RegExp",
1680        "Error",
1681    ];
1682    if NO_TAG.contains(&tag.as_str()) {
1683        return None;
1684    }
1685    Some(tag)
1686}
1687
1688/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
1689/// Every builtin exotic object reports its own brand, which is how packages
1690/// type-test values they did not construct (`toString.call(x) ===
1691/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
1692/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
1693fn object_tag(h: &host::JsHost, v: &Value) -> String {
1694    format!("[object {}]", object_brand(h, v))
1695}
1696
1697/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
1698/// …), without the `[object …]` wrapper. Split out so the brand and the
1699/// `Symbol.toStringTag` property read cannot disagree about what a value is.
1700fn object_brand(h: &host::JsHost, v: &Value) -> String {
1701    let tag: String = match v {
1702        Value::Undef => "Undefined".into(),
1703        Value::Bool(_) => "Boolean".into(),
1704        Value::Int(_) | Value::Float(_) => "Number".into(),
1705        Value::Str(_) => "String".into(),
1706        Value::Obj(_) => match h.get(v) {
1707            Some(JsObj::Null) => "Null".into(),
1708            Some(JsObj::Str(_)) => "String".into(),
1709            Some(JsObj::Array(_)) => "Array".into(),
1710            // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
1711            // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
1712            // {}))` is `'[object Array]'`. Everything else about a proxy brands
1713            // as a plain Object (a `Symbol.toStringTag` read through the `get`
1714            // trap is handled by the caller, before this).
1715            Some(JsObj::Proxy { target, .. }) => {
1716                let mut cur = target;
1717                for _ in 0..100 {
1718                    match h.get(cur) {
1719                        Some(JsObj::Proxy { target: t, .. }) => cur = t,
1720                        _ => break,
1721                    }
1722                }
1723                match h.get(cur) {
1724                    Some(JsObj::Array(_)) => "Array".into(),
1725                    _ => "Object".into(),
1726                }
1727            }
1728            // `function*` / `async function` / `async function*` carry their own
1729            // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
1730            Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
1731                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
1732                Some(d) if d.is_generator => "GeneratorFunction".into(),
1733                Some(d) if d.is_async => "AsyncFunction".into(),
1734                _ => "Function".into(),
1735            },
1736            // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
1737            // brand by name (21.3.1.9, 25.5.3, 28.1.14).
1738            Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
1739                n.clone()
1740            }
1741            Some(JsObj::Class(_))
1742            | Some(JsObj::Builtin(_))
1743            | Some(JsObj::BoundFunc { .. })
1744            | Some(JsObj::BoundMethod { .. }) => "Function".into(),
1745            // A suspended generator object is `[object Generator]`; an async one
1746            // `[object AsyncGenerator]`.
1747            Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
1748            Some(JsObj::Generator { .. }) => "Generator".into(),
1749            Some(JsObj::RegExp(_)) => "RegExp".into(),
1750            Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
1751            Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
1752            Some(JsObj::Promise { .. }) => "Promise".into(),
1753            Some(JsObj::Symbol { .. }) => "Symbol".into(),
1754            Some(JsObj::BigInt(_)) => "BigInt".into(),
1755            // Native-tagged instances brand by their tag; a typed array brands by
1756            // its element kind (`@@kind`), and every Error subclass is `Error`.
1757            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
1758                Some("TypedArray") => p
1759                    .get("@@kind")
1760                    .map(|k| h.str_of(k))
1761                    .unwrap_or_else(|| "Uint8Array".into()),
1762                Some("Buffer") => "Uint8Array".into(),
1763                // Every native class that really carries a `Symbol.toStringTag`
1764                // in Node brands by its own name. Verified against node v26:
1765                // `Object.prototype.toString.call(new WeakRef({}))` is
1766                // `[object WeakRef]`. The rest of the `@@native` tags
1767                // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
1768                // classes with NO tag, so they stay `[object Object]` — listing
1769                // them here would invent a brand Node does not have.
1770                Some(
1771                    t @ ("ArrayBuffer"
1772                    | "DataView"
1773                    | "Date"
1774                    | "WeakRef"
1775                    | "FinalizationRegistry"
1776                    | "TextEncoder"
1777                    | "TextDecoder"
1778                    | "URL"
1779                    | "URLSearchParams"),
1780                ) => t.into(),
1781                _ if h.error_to_string(v).is_some() => "Error".into(),
1782                _ => "Object".into(),
1783            },
1784            _ => "Object".into(),
1785        },
1786        // node-js only produces the Value variants above; fusevm's shell-oriented
1787        // variants never arise here.
1788        _ => "Object".into(),
1789    };
1790    tag
1791}
1792
1793fn b_setattr(vm: &mut VM, _: u8) -> Value {
1794    let val = vm.pop();
1795    let name = sval(&vm.pop());
1796    let recv = vm.pop();
1797    if let Err(e) = set_property(&recv, &name, val.clone()) {
1798        return abort(vm, e);
1799    }
1800    val
1801}
1802
1803/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
1804/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
1805/// `class C { static [k] = function(){} }`.
1806///
1807/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
1808/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
1809/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
1810/// unconditionally here.
1811///
1812/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
1813/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
1814fn b_named_eval(vm: &mut VM, _: u8) -> Value {
1815    let func = vm.pop();
1816    let kind = vm.pop().to_int();
1817    let key = vm.pop();
1818    let key = sval(&key);
1819    // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
1820    // description gives the empty name, not `[undefined]`.
1821    let base = match with_host(|h| h.symbol_of_key(&key)) {
1822        Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
1823            Some(JsObj::Symbol {
1824                desc: Some(desc), ..
1825            }) => format!("[{desc}]"),
1826            _ => String::new(),
1827        },
1828        None => key,
1829    };
1830    let name = match kind {
1831        host::member::GET => format!("get {base}"),
1832        host::member::SET => format!("set {base}"),
1833        _ => base,
1834    };
1835    with_host(|h| {
1836        let s = h.new_str(name);
1837        h.set_fn_prop(&func, "name", s);
1838    });
1839    func
1840}
1841
1842/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
1843/// on the same path a plain `o.k = v` takes.
1844pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
1845    set_property(recv, name, val)
1846}
1847
1848fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
1849    // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
1850    // element. The class's own field initializers install theirs directly
1851    // (`host::init_one_field`), so a declaration never reaches this check.
1852    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
1853        return Err(private_brand_message(name, true));
1854    }
1855    // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
1856    if crate::proxy::set(recv, name, &val, recv)? {
1857        return Ok(());
1858    }
1859    // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
1860    // back. Writing only the own property left the two views disagreeing:
1861    // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
1862    if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
1863        with_host(|h| h.set_name(name, val.clone()));
1864    }
1865    // `obj.__proto__ = p` re-links the prototype — but only for the two values
1866    // the Annex B setter accepts, an Object or `null`. Everything else is a
1867    // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
1868    // untouched and creates no own key), and a null-prototype object inherits
1869    // no such setter at all, so there the assignment is an ORDINARY own
1870    // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
1871    // prototype to the number 5.
1872    if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
1873        if with_host(|h| h.has_null_proto(recv)) {
1874            // falls through to the ordinary own-property write below
1875        } else {
1876            let assignable =
1877                with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
1878            if assignable {
1879                with_host(|h| h.set_proto(recv, val));
1880            }
1881            return Ok(());
1882        }
1883    }
1884    // A non-writable own property, or a new key on a non-extensible object,
1885    // silently discards the write (sloppy mode — the mode every script runs in).
1886    if !with_host(|h| h.can_write_prop(recv, name)) {
1887        return Ok(());
1888    }
1889    // An inherited/own setter accessor intercepts the write.
1890    if let Some((_, Some(setter))) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1891        let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
1892        return Ok(());
1893    }
1894    // A set-only-elsewhere getter (accessor with no setter): ignore the write.
1895    if let Some((Some(_), None)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1896        return Ok(());
1897    }
1898    // Writing `name`/`prototype`/statics on a function value.
1899    if matches!(
1900        with_host(|h| h.kind_of(recv)),
1901        Some(ObjKind::Func) | Some(ObjKind::Class)
1902    ) {
1903        with_host(|h| h.set_fn_prop(recv, name, val));
1904        return Ok(());
1905    }
1906    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
1907    // Each bare reference is a fresh `Builtin` handle, so route to the stable
1908    // per-namespace side table rather than the per-index `fn_props`.
1909    if let Some(ns) = peek(recv, |o| match o {
1910        JsObj::Builtin(ns) => Some(ns.clone()),
1911        _ => None,
1912    }) {
1913        // `process.exitCode` is an accessor in Node, not a data property: the
1914        // setter validates and stores the code the process will finally exit
1915        // with. Landing it in the generic static table made it a write-only
1916        // decoration — `process.exitCode = 3` read back as 3 and the process
1917        // still exited 0.
1918        if ns == "process" && name == "exitCode" {
1919            return crate::stdlib::process::set_exit_code(&val);
1920        }
1921        with_host(|h| h.set_builtin_static(&ns, name, val));
1922        return Ok(());
1923    }
1924    // `re.lastIndex = n` on a RegExp advances/resets its match cursor.
1925    if name == "lastIndex" {
1926        if let Some(n) = with_host(|h| match h.get(recv) {
1927            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
1928            _ => None,
1929        }) {
1930            with_host(|h| {
1931                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
1932                    r.last_index = if n.is_finite() && n >= 0.0 {
1933                        crate::utf16::U16Index::new(n as usize)
1934                    } else {
1935                        crate::utf16::U16Index::ZERO
1936                    };
1937                }
1938            });
1939            return Ok(());
1940        }
1941    }
1942    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
1943    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
1944        let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
1945        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
1946            return Ok(());
1947        }
1948        // `buf[i] = n` writes through to the Buffer's hidden byte array.
1949        if crate::stdlib::buffer::byte_set(recv, name, &val) {
1950            return Ok(());
1951        }
1952    }
1953    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
1954    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
1955        && name != "length"
1956        && name.parse::<usize>().is_err()
1957    {
1958        with_host(|h| h.set_fn_prop(recv, name, val));
1959        return Ok(());
1960    }
1961    // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
1962    // and does so outside the host borrow because `ToNumber` may run a user
1963    // `valueOf`. An invalid length throws instead of being silently coerced to 0.
1964    let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
1965        Some(host::to_array_length(&val)?)
1966    } else {
1967        None
1968    };
1969    with_host(|h| match h.get_mut(recv) {
1970        Some(JsObj::Object(props)) => {
1971            // Adding a *new* array-index key must re-place it into ascending
1972            // integer-key order (updating an existing key keeps its position).
1973            let is_new = !props.contains_key(name);
1974            props.insert(name.to_string(), val);
1975            if is_new && host::array_index(name).is_some() {
1976                host::canonicalize_own_keys(props);
1977            }
1978        }
1979        Some(JsObj::Array(items)) => {
1980            if let Some(n) = new_len {
1981                // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
1982                // just the one own key); shrinking drops any hole past the end.
1983                let old = items.len();
1984                items.resize(n, Value::Undef);
1985                if n > old {
1986                    h.mark_hole_range(recv, old..n);
1987                } else {
1988                    h.truncate_holes(recv, n);
1989                }
1990            } else if let Ok(i) = name.parse::<usize>() {
1991                // A write PAST the end leaves the skipped positions elided.
1992                let old = items.len();
1993                if i >= old {
1994                    items.resize(i + 1, Value::Undef);
1995                }
1996                items[i] = val;
1997                if i > old {
1998                    h.mark_hole_range(recv, old..i);
1999                }
2000                // …and the written index itself is no longer one. This is the
2001                // single site that keeps a hole record from outliving the
2002                // elision it describes: every array element write in the
2003                // language reaches it.
2004                h.clear_hole(recv, i);
2005            }
2006        }
2007        _ => {}
2008    });
2009    Ok(())
2010}
2011
2012fn b_getitem(vm: &mut VM, _: u8) -> Value {
2013    let idx = vm.pop();
2014    let recv = vm.pop();
2015    let key = match host::to_property_key(&idx) {
2016        Ok(k) => k,
2017        Err(e) => return abort(vm, e),
2018    };
2019    match get_property(&recv, &key) {
2020        Ok(v) => v,
2021        Err(e) => abort(vm, e),
2022    }
2023}
2024
2025fn b_setitem(vm: &mut VM, _: u8) -> Value {
2026    let val = vm.pop();
2027    let idx = vm.pop();
2028    let recv = vm.pop();
2029    let key = match host::to_property_key(&idx) {
2030        Ok(k) => k,
2031        Err(e) => return abort(vm, e),
2032    };
2033    if let Err(e) = set_property(&recv, &key, val.clone()) {
2034        return abort(vm, e);
2035    }
2036    val
2037}
2038
2039/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
2040/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
2041/// the three cannot drift. Reports `false` for a non-configurable property
2042/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
2043/// which is also what deleting an absent key reports.
2044pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
2045    // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
2046    // throw — the reason this reports a `Result` rather than a bare `bool`.
2047    if let Some(b) = crate::proxy::delete(recv, key)? {
2048        return Ok(b);
2049    }
2050    // `delete require.cache[id]` drops the module so the next `require` of that
2051    // file runs it again — the whole point of exposing the cache.
2052    if peek(recv, |o| match o {
2053        JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
2054        _ => None,
2055    }) == Some(true)
2056    {
2057        return Ok(crate::module::cache_delete(key));
2058    }
2059    if !with_host(|h| h.prop_attrs(recv, key).configurable) {
2060        return Ok(false);
2061    }
2062    with_host(|h| {
2063        let index = key.parse::<usize>();
2064        match h.get_mut(recv) {
2065            Some(JsObj::Object(props)) => {
2066                props.shift_remove(key);
2067                return;
2068            }
2069            Some(JsObj::Array(items)) => {
2070                if let Ok(i) = index {
2071                    if i < items.len() {
2072                        // `delete a[i]` punches a HOLE: the length is unchanged
2073                        // but the index stops being an own property.
2074                        items[i] = Value::Undef;
2075                        h.mark_hole(recv, i);
2076                    }
2077                    return;
2078                }
2079            }
2080            _ => {}
2081        }
2082        // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
2083        // a function/class, is an ordinary own property kept in the side table.
2084        h.remove_fn_prop(recv, key);
2085    });
2086    Ok(true)
2087}
2088
2089fn b_delitem(vm: &mut VM, _: u8) -> Value {
2090    let idx = vm.pop();
2091    let recv = vm.pop();
2092    // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
2093    // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
2094    // description and delete a key nothing ever wrote.
2095    let key = match host::to_property_key(&idx) {
2096        Ok(k) => k,
2097        Err(e) => return abort(vm, e),
2098    };
2099    match delete_property(&recv, &key) {
2100        Ok(b) => Value::Bool(b),
2101        Err(e) => abort(vm, e),
2102    }
2103}
2104
2105fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
2106    let name = sval(&vm.pop());
2107    let recv = vm.pop();
2108    match delete_property(&recv, &name) {
2109        Ok(b) => Value::Bool(b),
2110        Err(e) => abort(vm, e),
2111    }
2112}
2113
2114// ── constructors ──────────────────────────────────────────────────────────────
2115
2116fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
2117    let parts = pop_n(vm, argc as usize);
2118    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
2119    with_host(|h| h.new_str(s))
2120}
2121
2122fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
2123    let items = pop_n(vm, argc as usize);
2124    with_host(|h| h.new_array(items))
2125}
2126
2127/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
2128/// only for an array literal that actually contains an elision, so a dense
2129/// literal costs nothing. Returns `undefined`; the array stays on the stack
2130/// underneath (the compiler `Dup`s it).
2131fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
2132    let idx = vm.pop();
2133    let arr = vm.pop();
2134    let i = match idx {
2135        Value::Int(i) if i >= 0 => i as usize,
2136        _ => return Value::Undef,
2137    };
2138    with_host(|h| h.mark_hole(&arr, i));
2139    Value::Undef
2140}
2141
2142fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
2143    let flat = pop_n(vm, argc as usize);
2144    let mut props: IndexMap<String, Value> = IndexMap::new();
2145    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
2146    let mut proto_override: Option<Value> = None;
2147    let mut i = 0;
2148    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
2149        if i + 2 >= flat.len() {
2150            break;
2151        }
2152        // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
2153        // the literal reserves its slot here with the `@@ord:` marker key that
2154        // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
2155        // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
2156        // marker landed at the end.
2157        if matches!(flat[i], Value::Int(2)) {
2158            let key = with_host(|h| h.str_of(&flat[i + 1]));
2159            props
2160                .entry(format!("{}{key}", host::ORD_MARKER))
2161                .or_insert(Value::Undef);
2162            i += 3;
2163            continue;
2164        }
2165        let spread = matches!(flat[i], Value::Int(1));
2166        if spread {
2167            let src = flat[i + 1].clone();
2168            // A STRING source spreads its index properties (`{..."ab"}` is
2169            // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
2170            // String exotic object owns one enumerable property per UTF-16 code
2171            // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
2172            // a string source contributed nothing and `{..."ab"}` was `{}`.
2173            // Every other primitive (number/boolean/symbol) boxes to an object
2174            // with no own enumerable properties, and null/undefined are ignored,
2175            // so those correctly stay no-ops on the path below.
2176            if let Some(s) = with_host(|h| h.as_str(&src)) {
2177                for idx in 0..crate::utf16::len(&s) {
2178                    if let Ok(ch) = get_property(&src, &idx.to_string()) {
2179                        props.insert(idx.to_string(), ch);
2180                    }
2181                }
2182                i += 3;
2183                continue;
2184            }
2185            // Object spread copies own *enumerable* properties only — never the
2186            // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
2187            // into something that still claimed to be a Buffer) and never a
2188            // property a descriptor marked non-enumerable.
2189            let entries = host::own_enum_entries_deep(&src);
2190            for (k, v) in entries {
2191                props.insert(k, v);
2192            }
2193            // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
2194            // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
2195            for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
2196                props.insert(k, v);
2197            }
2198        } else {
2199            let key = with_host(|h| h.str_of(&flat[i + 1]));
2200            if key == "__proto__" {
2201                proto_override = Some(flat[i + 2].clone());
2202            } else {
2203                props.insert(key, flat[i + 2].clone());
2204            }
2205        }
2206        i += 3;
2207    }
2208    with_host(|h| {
2209        let o = h.new_object(props);
2210        if let Some(p) = proto_override {
2211            if matches!(p, Value::Obj(_)) {
2212                h.set_proto(&o, p);
2213            }
2214        }
2215        o
2216    })
2217}
2218
2219fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
2220    let def_id = match vm.pop() {
2221        Value::Int(n) => n as usize,
2222        Value::Float(f) => f as usize,
2223        _ => return abort(vm, "internal: MKFUNC id".into()),
2224    };
2225    let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
2226        Some(d) => (
2227            d.is_arrow,
2228            (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
2229        ),
2230        None => (false, None),
2231    });
2232    with_host(|h| {
2233        let mut env = h.current_env_capture();
2234        let this = h.current_this();
2235        // A named function expression closes over an extra scope holding its own
2236        // name, so the body can recurse through it (`function f(){ … f() … }`)
2237        // independently of whatever the outer binding is later set to.
2238        if self_name.is_some() {
2239            env = host::child_env(env);
2240        }
2241        let f = h.alloc(JsObj::Func(FuncVal {
2242            def_id,
2243            env: Some(env.clone()),
2244            this,
2245            is_arrow,
2246            home_class: None,
2247        }));
2248        if let Some(n) = self_name {
2249            env.borrow_mut().vars.insert(n, f.clone());
2250        }
2251        f
2252    })
2253}
2254
2255// ── truthiness / coercion / equality ──────────────────────────────────────────
2256
2257fn b_truthy(vm: &mut VM, _: u8) -> Value {
2258    let v = vm.pop();
2259    Value::Bool(with_host(|h| h.truthy(&v)))
2260}
2261
2262fn b_nullish(vm: &mut VM, _: u8) -> Value {
2263    let v = vm.pop();
2264    Value::Bool(with_host(|h| h.is_nullish(&v)))
2265}
2266
2267fn b_tostr(vm: &mut VM, _: u8) -> Value {
2268    let v = vm.pop();
2269    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
2270    // `String(x)`, object keys).
2271    match host::to_string_value(&v) {
2272        Ok(s) => s,
2273        Err(e) => abort(vm, e),
2274    }
2275}
2276
2277fn b_typeof(vm: &mut VM, _: u8) -> Value {
2278    let v = vm.pop();
2279    with_host(|h| {
2280        let t = h.type_of(&v);
2281        h.new_str(t)
2282    })
2283}
2284
2285/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
2286/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
2287fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
2288    let name = sval(&vm.pop());
2289    // Bound name (user variable) → typeof its value.
2290    if let Some(v) = with_host(|h| h.read_name(&name)) {
2291        return with_host(|h| {
2292            let t = h.type_of(&v);
2293            h.new_str(t)
2294        });
2295    }
2296    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
2297    // would produce, then take its type (so object-namespaces like `console`/
2298    // `Math`/`JSON`/`process` report "object", constructors report "function").
2299    let t = match name.as_str() {
2300        "undefined" => "undefined".to_string(),
2301        "NaN" | "Infinity" => "number".to_string(),
2302        "globalThis" | "global" => "object".to_string(),
2303        n if is_namespace(n) || is_known_builtin(n) => {
2304            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
2305            with_host(|h| h.type_of(&v)).to_string()
2306        }
2307        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
2308    };
2309    with_host(|h| h.new_str(t))
2310}
2311
2312fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
2313    let b = vm.pop();
2314    let a = vm.pop();
2315    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
2316}
2317
2318fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
2319    let b = vm.pop();
2320    let a = vm.pop();
2321    // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
2322    // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
2323    // runs before the host borrow. Object ⇄ object stays a reference check.
2324    let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
2325        (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
2326            Ok(p) => (p, b),
2327            Err(e) => return abort(vm, e),
2328        },
2329        (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
2330            Ok(p) => (a, p),
2331            Err(e) => return abort(vm, e),
2332        },
2333        _ => (a, b),
2334    };
2335    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
2336}
2337
2338fn b_instanceof(vm: &mut VM, _: u8) -> Value {
2339    let ctor = vm.pop();
2340    let obj = vm.pop();
2341    match host::instance_of(&obj, &ctor) {
2342        Ok(b) => Value::Bool(b),
2343        Err(e) => abort(vm, e),
2344    }
2345}
2346
2347// ── bitwise / unary ───────────────────────────────────────────────────────────
2348
2349fn b_binop(vm: &mut VM, _: u8) -> Value {
2350    let b = vm.pop();
2351    let a = vm.pop();
2352    let tag = match vm.pop() {
2353        Value::Int(n) => n,
2354        _ => 0,
2355    };
2356    // Both operands are ToPrimitive-d with the number hint before ToInt32
2357    // (ECMA-262 13.12.1), which has to happen outside the host borrow.
2358    let r = host::to_primitive(&a, "number")
2359        .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
2360        .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
2361    finish(vm, r)
2362}
2363
2364fn b_unary(vm: &mut VM, _: u8) -> Value {
2365    let v = vm.pop();
2366    let tag = match vm.pop() {
2367        Value::Int(n) => n,
2368        _ => 0,
2369    };
2370    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
2371    // computed in arbitrary precision.
2372    if with_host(|h| h.is_bigint_val(&v)) {
2373        return match tag {
2374            host::unop::POS => abort(
2375                vm,
2376                host::type_error("Cannot convert a BigInt value to a number"),
2377            ),
2378            host::unop::BITNOT => {
2379                let b = with_host(|h| h.as_bigint(&v)).unwrap();
2380                let r = -(b + num_bigint::BigInt::from(1));
2381                with_host(|h| h.new_bigint(r))
2382            }
2383            _ => Value::Undef,
2384        };
2385    }
2386    // `ToNumber` outside the host borrow: an object operand's `valueOf` /
2387    // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
2388    let n = match host::to_number_value(&v) {
2389        Ok(n) => n,
2390        Err(e) => return abort(vm, e),
2391    };
2392    match tag {
2393        host::unop::POS => Value::Float(n),
2394        host::unop::BITNOT => {
2395            let i = if n.is_finite() {
2396                n.trunc() as i64 as i32
2397            } else {
2398                0
2399            };
2400            Value::Float(!i as f64)
2401        }
2402        _ => Value::Undef,
2403    }
2404}
2405
2406// ── membership ────────────────────────────────────────────────────────────────
2407
2408fn b_contains(vm: &mut VM, _: u8) -> Value {
2409    let container = vm.pop();
2410    let key = vm.pop();
2411    // `x in y` requires y to be an object. V8 names both operands:
2412    // `Cannot use 'in' operator to search for 'a' in 5`.
2413    if !matches!(container, Value::Obj(_)) {
2414        let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
2415        return abort(
2416            vm,
2417            host::type_error(&format!(
2418                "Cannot use 'in' operator to search for '{k}' in {c}"
2419            )),
2420        );
2421    }
2422    let k = with_host(|h| h.property_key(&key));
2423    match has_property(&container, &k) {
2424        Ok(b) => Value::Bool(b),
2425        Err(e) => abort(vm, e),
2426    }
2427}
2428
2429// ── control ───────────────────────────────────────────────────────────────────
2430
2431fn b_sig_return(vm: &mut VM, _: u8) -> Value {
2432    let v = vm.pop();
2433    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
2434    vm.ip = vm.chunk.ops.len();
2435    v
2436}
2437
2438/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
2439/// inside a `try` block, which the host runs as its own chunk). Raise the signal
2440/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
2441fn b_sig_break(vm: &mut VM, _: u8) -> Value {
2442    let label = sval(&vm.pop());
2443    let label = (!label.is_empty()).then_some(label);
2444    with_host(|h| h.signal = Some(host::Signal::Break(label)));
2445    vm.ip = vm.chunk.ops.len();
2446    Value::Undef
2447}
2448
2449/// `continue [label]` out of a `try` block — see [`b_sig_break`].
2450fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
2451    let label = sval(&vm.pop());
2452    let label = (!label.is_empty()).then_some(label);
2453    with_host(|h| h.signal = Some(host::Signal::Continue(label)));
2454    vm.ip = vm.chunk.ops.len();
2455    Value::Undef
2456}
2457
2458/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
2459/// describes what the `try` is nested in (see [`host::unwind`]):
2460///
2461/// * no signal → `NONE`, execution continues normally;
2462/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
2463///   keeps travelling outward;
2464/// * `break`/`continue` targeting the enclosing loop → consume it and report
2465///   `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
2466///   continue target;
2467/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
2468///   the signal pending, so leaving this loop re-dispatches it one level out.
2469fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
2470    let cont_tag = sval(&vm.pop());
2471    let brk_tag = sval(&vm.pop());
2472    let sig = match with_host(|h| h.signal.clone()) {
2473        Some(s) => s,
2474        None => return Value::Int(host::unwind::NONE),
2475    };
2476    // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
2477    let propagate = |vm: &mut VM| {
2478        vm.ip = vm.chunk.ops.len();
2479        Value::Int(host::unwind::NONE)
2480    };
2481    match &sig {
2482        host::Signal::Return(_) => propagate(vm),
2483        host::Signal::Break(label) => {
2484            if brk_tag == host::unwind::NO_LOOP {
2485                return propagate(vm);
2486            }
2487            let mine = match label {
2488                None => true, // unlabeled: always the innermost enclosing context
2489                Some(l) => brk_tag == *l,
2490            };
2491            if mine {
2492                with_host(|h| h.signal = None);
2493            }
2494            // Not ours: still leave this context by its break exit, keeping the
2495            // signal pending for the next dispatch point one level out.
2496            Value::Int(host::unwind::BREAK)
2497        }
2498        host::Signal::Continue(label) => {
2499            let mine = match label {
2500                // Unlabeled `continue` binds to the innermost continue-catching
2501                // loop — which a `switch` between here and it is NOT.
2502                None => cont_tag != host::unwind::NO_LOOP,
2503                Some(l) => cont_tag == *l,
2504            };
2505            if mine {
2506                with_host(|h| h.signal = None);
2507                return Value::Int(host::unwind::CONTINUE);
2508            }
2509            if brk_tag == host::unwind::NO_LOOP {
2510                return propagate(vm);
2511            }
2512            // The target loop is further out: exit the innermost context here and
2513            // re-dispatch there.
2514            Value::Int(host::unwind::BREAK)
2515        }
2516    }
2517}
2518
2519fn b_throw(vm: &mut VM, _: u8) -> Value {
2520    let v = vm.pop();
2521    let msg = with_host(|h| {
2522        h.exc = Some(v.clone());
2523        // Prefer an error object's message for the top-level report.
2524        error_display(h, &v)
2525    });
2526    abort(vm, msg)
2527}
2528
2529fn error_display(h: &host::JsHost, v: &Value) -> String {
2530    if let Some(JsObj::Object(props)) = h.get(v) {
2531        let name = props
2532            .get("name")
2533            .map(|x| h.str_of(x))
2534            .unwrap_or_else(|| "Error".into());
2535        if let Some(m) = props.get("message") {
2536            return format!("Uncaught {name}: {}", h.str_of(m));
2537        }
2538    }
2539    format!("Uncaught {}", h.str_of(v))
2540}
2541
2542fn b_try(vm: &mut VM, _: u8) -> Value {
2543    let id = match vm.pop() {
2544        Value::Int(n) => n as usize,
2545        _ => return abort(vm, "internal: TRY id".into()),
2546    };
2547    // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
2548    // its handler and its finalizer bytecode — every time control entered it,
2549    // which for a `try` inside a loop is once per iteration.
2550    let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
2551        Some(t) => t,
2552        None => return abort(vm, "internal: unknown try id".into()),
2553    };
2554    let mut pending: Option<String> = None;
2555    // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
2556    // through can leave block scopes open. Snapshot the scope and restore it
2557    // before the handler and after the whole statement.
2558    let scope = with_host(|h| h.scope_snapshot());
2559
2560    with_host(|h| h.push_scope()); // the try block is its own block scope
2561    let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
2562        with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
2563    });
2564    with_host(|h| h.restore_scope(scope.clone()));
2565    let signal_after = with_host(|h| h.signal.is_some());
2566    if let Err(e) = body_res {
2567        if signal_after {
2568            pending = Some(e);
2569        } else if has_handler {
2570            // Bind the thrown value (or a synthesized error) to the catch param.
2571            let thrown =
2572                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
2573            with_host(|h| {
2574                h.error = None;
2575                h.exc = None;
2576            });
2577            // The catch parameter is block-scoped to the handler.
2578            with_host(|h| h.push_scope());
2579            if let Some(name) = &catch_bind {
2580                with_host(|h| h.declare_name(name, thrown));
2581            }
2582            let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
2583                with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
2584            });
2585            with_host(|h| h.restore_scope(scope.clone()));
2586            if let Err(e2) = hres {
2587                pending = Some(e2);
2588            }
2589        } else {
2590            pending = Some(e);
2591        }
2592    }
2593
2594    // finally always runs; a finally error/signal supersedes.
2595    if has_finalizer {
2596        let sig_before = with_host(|h| h.signal.take());
2597        with_host(|h| h.push_scope()); // ditto for `finally`
2598        let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
2599            with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
2600        });
2601        with_host(|h| h.restore_scope(scope.clone()));
2602        match fres {
2603            Ok(_) => {
2604                if with_host(|h| h.signal.is_none()) {
2605                    // The finalizer completed normally: the try/catch block's own
2606                    // abrupt completion resumes.
2607                    with_host(|h| h.signal = sig_before);
2608                } else {
2609                    // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
2610                    // completion is abrupt (`return`/`break`/`continue` inside
2611                    // `finally`), that completion REPLACES the try/catch block's —
2612                    // including a pending throw, which is discarded, not rethrown.
2613                    pending = None;
2614                    with_host(|h| {
2615                        h.error = None;
2616                        h.exc = None;
2617                    });
2618                }
2619            }
2620            Err(e) => pending = Some(e),
2621        }
2622    }
2623
2624    if let Some(e) = pending {
2625        return abort(vm, e);
2626    }
2627    Value::Undef
2628}
2629
2630/// Synthesize an `Error`-shaped object from an internal error string, linked to
2631/// the matching builtin error prototype so `instanceof`/`.constructor` work.
2632pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
2633    h.ensure_error_protos();
2634    // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
2635    // error class, exactly as Node's internal errors render it in `.stack`.
2636    let (head, rest) = match e.split_once(": ") {
2637        Some((n, m)) => (n, m.to_string()),
2638        None => ("", e.to_string()),
2639    };
2640    let (base, code) = match head.split_once(" [") {
2641        Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
2642        _ => (head, None),
2643    };
2644    let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
2645        (base.to_string(), rest)
2646    } else {
2647        ("Error".to_string(), e.to_string())
2648    };
2649    // A `host::plain_coded_error` marker: the code rides at the head of the
2650    // MESSAGE rather than in the class, because Node's native-layer errors set
2651    // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
2652    // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
2653    // internal and must never reach a user-visible `.message`.
2654    let mut code = code;
2655    // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
2656    // bracketed head, false for the marker form.
2657    let mut bracketed = code.is_some();
2658    if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
2659        if let Some((c, m)) = rest.split_once('\u{1}') {
2660            code = Some(c.to_string());
2661            bracketed = false;
2662            message = m.to_string();
2663        }
2664    }
2665    let mut props: IndexMap<String, Value> = IndexMap::new();
2666    let mv = h.new_str(message.clone());
2667    props.insert("message".into(), mv);
2668    if let Some(c) = &code {
2669        let cv = h.new_str(c.clone());
2670        props.insert("code".into(), cv);
2671        if bracketed {
2672            // Marks this as a Node JS-layer error, whose `toString` brackets the
2673            // code. A native-layer error has the same `.code` and does not.
2674            props.insert("@@nodeError".into(), Value::Bool(true));
2675        }
2676    }
2677    let label = match (&code, bracketed) {
2678        (Some(c), true) => format!("{name} [{c}]"),
2679        _ => name.clone(),
2680    };
2681    let frames = h.stack_frames();
2682    let stack = if message.is_empty() {
2683        format!("{label}{frames}")
2684    } else {
2685        format!("{label}: {message}{frames}")
2686    };
2687    let sv = h.new_str(stack);
2688    props.insert("stack".into(), sv);
2689    // A libuv system-error message is itself the canonical encoding of the
2690    // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
2691    // filesystem/network failure recovers the enumerable `code`/`errno`/
2692    // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
2693    // single most common error-handling idiom in Node packages) depend on.
2694    for (k, v) in syscall_error_fields(&message) {
2695        let sv = match v {
2696            SysField::Str(s) => h.new_str(s),
2697            SysField::Num(n) => Value::Float(n),
2698        };
2699        props.insert(k.into(), sv);
2700    }
2701    let obj = h.new_object(props);
2702    if let Some(p) = host::error_proto_of(h, &name) {
2703        h.set_proto(&obj, p);
2704    }
2705    // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
2706    // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
2707    h.hide_prop(&obj, "message");
2708    h.hide_prop(&obj, "stack");
2709    obj
2710}
2711
2712enum SysField {
2713    Str(String),
2714    Num(f64),
2715}
2716
2717/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
2718/// own properties Node hangs off a system error. Returns empty for any message
2719/// that is not in that shape.
2720fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
2721    let (code, rest) = match message.split_once(": ") {
2722        Some((c, r))
2723            if c.len() >= 2
2724                && c.starts_with('E')
2725                && c.bytes()
2726                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
2727        {
2728            (c, r)
2729        }
2730        _ => return Vec::new(),
2731    };
2732    let mut out: Vec<(&'static str, SysField)> = vec![
2733        ("errno", SysField::Num(errno_for(code))),
2734        ("code", SysField::Str(code.to_string())),
2735    ];
2736    // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
2737    if let Some((_, tail)) = rest.split_once(", ") {
2738        let (syscall, path) = match tail.split_once(" '") {
2739            Some((s, p)) => (s, p.strip_suffix('\'')),
2740            None => (tail, None),
2741        };
2742        out.push(("syscall", SysField::Str(syscall.to_string())));
2743        if let Some(p) = path {
2744            out.push(("path", SysField::Str(p.to_string())));
2745        }
2746    }
2747    out
2748}
2749
2750/// The negative `errno` Node reports for a libuv error code on this platform.
2751/// Only the codes `err_str` can produce are mapped; anything else reports the
2752/// generic `EIO` number rather than inventing a value.
2753fn errno_for(code: &str) -> f64 {
2754    let n: i32 = match code {
2755        "ENOENT" => 2,
2756        "EACCES" => 13,
2757        "EEXIST" => 17,
2758        "ENOTDIR" => 20,
2759        "EISDIR" => 21,
2760        "EINVAL" => 22,
2761        "EPIPE" => 32,
2762        "ENOTEMPTY" => 66,
2763        _ => 5, // EIO
2764    };
2765    -f64::from(n)
2766}
2767
2768// ── iteration ─────────────────────────────────────────────────────────────────
2769
2770fn b_getiter(vm: &mut VM, _: u8) -> Value {
2771    let v = vm.pop();
2772    // A generator is its own iterator (resumed lazily by FORITER).
2773    if with_host(|h| h.is_generator_val(&v)) {
2774        return v;
2775    }
2776    // A Proxy's iterator comes from its traps, materialized eagerly: the
2777    // `lookup_chain` probe below reads the property map a proxy does not have.
2778    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
2779        return match crate::proxy::iterate(&v) {
2780            Ok(Some(items)) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
2781            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
2782            Err(e) => abort(vm, e),
2783        };
2784    }
2785    // An object with a user `Symbol.iterator`: call it to get the iterator object.
2786    if let Some(iter_fn) = with_host(|h| host::lookup_chain(h, &v, "@@iterator")) {
2787        if with_host(|h| host::is_callable(h, &iter_fn)) {
2788            return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
2789                Ok(it) => it,
2790                Err(e) => abort(vm, e),
2791            };
2792        }
2793    }
2794    match with_host(|h| h.iter_vec(&v)) {
2795        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
2796        Err(e) => abort(vm, e),
2797    }
2798}
2799
2800fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
2801    let v = vm.pop();
2802    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
2803    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
2804    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
2805    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
2806        return match crate::proxy::own_enum_string_keys(&v) {
2807            Ok(keys) => with_host(|h| {
2808                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
2809                h.new_array(out)
2810            }),
2811            Err(e) => abort(vm, e),
2812        };
2813    }
2814    let keys = with_host(|h| h.enum_keys(&v));
2815    with_host(|h| h.new_array(keys))
2816}
2817
2818fn b_foriter(vm: &mut VM, _: u8) -> Value {
2819    let it = match vm.stack.last() {
2820        Some(v) => v.clone(),
2821        None => return abort(vm, "internal: FORITER with empty stack".into()),
2822    };
2823    // Eager array-backed iterator (arrays/strings/Map/Set).
2824    let eager = with_host(|h| {
2825        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
2826            if *idx < items.len() {
2827                let v = items[*idx].clone();
2828                *idx += 1;
2829                return Some(Some(v));
2830            }
2831            return Some(None);
2832        }
2833        None
2834    });
2835    if let Some(step) = eager {
2836        return match step {
2837            Some(v) => {
2838                vm.push(v);
2839                Value::Bool(true)
2840            }
2841            None => Value::Bool(false),
2842        };
2843    }
2844    // Generator: resume one step.
2845    if with_host(|h| h.is_generator_val(&it)) {
2846        return match host::gen_resume(&it, Value::Undef) {
2847            Ok(host::GenStep::Yield(v)) => {
2848                vm.push(v);
2849                Value::Bool(true)
2850            }
2851            Ok(host::GenStep::Done(_)) => Value::Bool(false),
2852            Err(e) => abort(vm, e),
2853        };
2854    }
2855    // A user iterator object with a `.next()` returning `{ value, done }`.
2856    match host::call_method(&it, "next", Vec::new()) {
2857        Ok(step) => {
2858            let done = get_property(&step, "done")
2859                .map(|d| with_host(|h| h.truthy(&d)))
2860                .unwrap_or(true);
2861            if done {
2862                Value::Bool(false)
2863            } else {
2864                match get_property(&step, "value") {
2865                    Ok(v) => {
2866                        vm.push(v);
2867                        Value::Bool(true)
2868                    }
2869                    Err(e) => abort(vm, e),
2870                }
2871            }
2872        }
2873        Err(e) => abort(vm, e),
2874    }
2875}
2876
2877fn b_unpack(vm: &mut VM, _: u8) -> Value {
2878    let star = match vm.pop() {
2879        Value::Int(n) => n,
2880        _ => -1,
2881    };
2882    let count = match vm.pop() {
2883        Value::Int(n) => n as usize,
2884        _ => 0,
2885    };
2886    let iterable = vm.pop();
2887    let items = match host::iter_all(&iterable) {
2888        Ok(v) => v,
2889        Err(e) => return abort(vm, e),
2890    };
2891    let ordered: Vec<Value> = if star < 0 {
2892        (0..count)
2893            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
2894            .collect()
2895    } else {
2896        let si = star as usize;
2897        let after = count.saturating_sub(si + 1);
2898        let rest_end = items.len().saturating_sub(after).max(si);
2899        let mut out: Vec<Value> = Vec::with_capacity(count);
2900        for i in 0..si {
2901            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
2902        }
2903        let rest: Vec<Value> = items
2904            .get(si..rest_end)
2905            .map(|s| s.to_vec())
2906            .unwrap_or_default();
2907        out.push(with_host(|h| h.new_array(rest)));
2908        for j in 0..after {
2909            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
2910        }
2911        out
2912    };
2913    if ordered.is_empty() {
2914        return Value::Undef;
2915    }
2916    for it in ordered[1..].iter().rev().cloned() {
2917        vm.push(it);
2918    }
2919    ordered[0].clone()
2920}
2921
2922fn b_build_args(vm: &mut VM, argc: u8) -> Value {
2923    let flat = pop_n(vm, argc as usize);
2924    let mut out = Vec::new();
2925    // Elided positions of an array literal (tag 2), recorded as the run-time
2926    // index each lands on — which only this walk knows, because a preceding
2927    // spread contributes an unknown number of elements. Call-argument lists,
2928    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
2929    // empty for them.
2930    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
2931    let mut i = 0;
2932    while i + 1 < flat.len() {
2933        let val = flat[i + 1].clone();
2934        match flat[i] {
2935            Value::Int(1) => match host::iter_all(&val) {
2936                Ok(items) => out.extend(items),
2937                Err(e) => return abort(vm, e),
2938            },
2939            Value::Int(2) => {
2940                holes.insert(out.len());
2941                out.push(Value::Undef);
2942            }
2943            _ => out.push(val),
2944        }
2945        i += 2;
2946    }
2947    with_host(|h| {
2948        let arr = h.new_array(out);
2949        h.install_holes(&arr, holes);
2950        arr
2951    })
2952}
2953
2954// ── calls ──────────────────────────────────────────────────────────────────────
2955
2956fn b_call(vm: &mut VM, argc: u8) -> Value {
2957    let mut args = pop_n(vm, argc as usize);
2958    let name = sval(&args.remove(0));
2959    let r = host::call_named(&name, args);
2960    // A bare name that resolved to a non-callable reports the VALUE
2961    // (`undefined is not a function`); node names the identifier. Resolving it
2962    // again to learn what the message said costs nothing off the error path.
2963    let r = r.map_err(|e| {
2964        let shown = global_binding(&name)
2965            .map(|v| with_host(|h| h.str_of(&v)))
2966            .unwrap_or_default();
2967        host::name_call_site(vm, &shown, e)
2968    });
2969    finish(vm, r)
2970}
2971
2972fn b_call_method(vm: &mut VM, argc: u8) -> Value {
2973    let mut args = pop_n(vm, argc as usize);
2974    let recv = args.remove(0);
2975    let name = sval(&args.remove(0));
2976    let r = host::call_method(&recv, &name, args);
2977    // `z.f()` on a missing method is `z.f is not a function` in node, not
2978    // `f is not a function`: V8 names the callee as the source wrote it. The
2979    // text was recorded for this op at compile time.
2980    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
2981    finish(vm, r)
2982}
2983
2984fn b_call_value(vm: &mut VM, argc: u8) -> Value {
2985    let mut args = pop_n(vm, argc as usize);
2986    let callable = args.remove(0);
2987    let r = host::invoke(&callable, args, None);
2988    // The callee here is an expression, not a name, so the message it produced
2989    // describes the VALUE (`undefined is not a function`); node names the
2990    // expression. Same site table, keyed on that rendering.
2991    let r = r.map_err(|e| {
2992        let shown = with_host(|h| h.str_of(&callable));
2993        host::name_call_site(vm, &shown, e)
2994    });
2995    finish(vm, r)
2996}
2997
2998fn b_new(vm: &mut VM, argc: u8) -> Value {
2999    let mut args = pop_n(vm, argc as usize);
3000    let ctor = args.remove(0);
3001    let r = host::construct(&ctor, args);
3002    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
3003    // call does.
3004    let r = r.map_err(|e| {
3005        let shown = with_host(|h| h.str_of(&ctor));
3006        host::name_call_site(vm, &shown, e)
3007    });
3008    finish(vm, r)
3009}
3010
3011fn b_apply(vm: &mut VM, _: u8) -> Value {
3012    let args_arr = vm.pop();
3013    let callable = vm.pop();
3014    let args = host::iter_all(&args_arr).unwrap_or_default();
3015    let r = host::invoke(&callable, args, None);
3016    finish(vm, r)
3017}
3018
3019fn b_apply_method(vm: &mut VM, _: u8) -> Value {
3020    let args_arr = vm.pop();
3021    let name = sval(&vm.pop());
3022    let recv = vm.pop();
3023    let args = host::iter_all(&args_arr).unwrap_or_default();
3024    let r = host::call_method(&recv, &name, args);
3025    finish(vm, r)
3026}
3027
3028// ── numeric hook ──────────────────────────────────────────────────────────────
3029
3030/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
3031/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
3032///
3033/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
3034/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
3035/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
3036/// rather than inside `JsHost::arith`, because calling back into JS re-enters
3037/// the VM and `arith` runs under the host's `RefCell` borrow.
3038pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
3039    use NumOp::*;
3040    let (a, b) = match op {
3041        // `==`/`!=` only convert when the OTHER side is a primitive that can be
3042        // compared numerically or textually; `{} == {}` stays a reference check.
3043        Eq | Ne => {
3044            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
3045            match (pa, pb) {
3046                (false, true) if coerces_against_object(b) => {
3047                    (host::to_primitive(a, "default")?, b.clone())
3048                }
3049                (true, false) if coerces_against_object(a) => {
3050                    (a.clone(), host::to_primitive(b, "default")?)
3051                }
3052                _ => (a.clone(), b.clone()),
3053            }
3054        }
3055        // `+` uses the default hint (`valueOf` first, but a string result still
3056        // selects concatenation); everything else uses the number hint.
3057        Add => (
3058            host::to_primitive(a, "default")?,
3059            host::to_primitive(b, "default")?,
3060        ),
3061        _ => (
3062            host::to_primitive(a, "number")?,
3063            host::to_primitive(b, "number")?,
3064        ),
3065    };
3066    reject_symbol_operand(op, &a, &b)?;
3067    with_host(|h| h.arith(op, &a, &b))
3068}
3069
3070/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
3071/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
3072/// concatenated `Symbol(desc)` into the result.
3073///
3074/// Which of the two messages V8 uses is decided by whether the operation is
3075/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
3076/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
3077/// `Symbol() + Symbol()` and `Symbol() * 1` are all
3078/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
3079/// (`Symbol() == 1` is `false`), so they are left alone.
3080fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
3081    use NumOp::*;
3082    if matches!(op, Eq | Ne) {
3083        return Ok(());
3084    }
3085    let (sym, concat) = with_host(|h| {
3086        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
3087        let is_str =
3088            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
3089        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
3090    });
3091    if !sym {
3092        return Ok(());
3093    }
3094    Err(host::type_error(if matches!(op, Add) && concat {
3095        "Cannot convert a Symbol value to a string"
3096    } else {
3097        "Cannot convert a Symbol value to a number"
3098    }))
3099}
3100
3101/// Whether a primitive `v` makes `==` against an object convert that object
3102/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
3103/// `undefined` and booleans are settled without a `ToPrimitive` call
3104/// (a boolean is coerced to a number first, and then it does).
3105fn coerces_against_object(v: &Value) -> bool {
3106    match v {
3107        Value::Undef => false,
3108        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
3109        _ => with_host(|h| !h.is_null(v)),
3110    }
3111}
3112
3113// ══ standard library ═══════════════════════════════════════════════════════════
3114
3115/// Namespaces reachable as bare globals.
3116fn is_namespace(name: &str) -> bool {
3117    matches!(
3118        name,
3119        "console"
3120            | "Math"
3121            | "JSON"
3122            | "Object"
3123            | "Array"
3124            | "Number"
3125            | "String"
3126            | "Boolean"
3127            | "Symbol"
3128            | "Reflect"
3129            | "Promise"
3130            | "process"
3131            | "Buffer"
3132            | "URL"
3133            | "URLSearchParams"
3134    )
3135}
3136
3137const GLOBAL_FUNCS: &[&str] = &[
3138    "parseInt",
3139    "parseFloat",
3140    "isNaN",
3141    "isFinite",
3142    "encodeURIComponent",
3143    "decodeURIComponent",
3144    "encodeURI",
3145    "decodeURI",
3146    // Annex B legacy encoders. Still globals on every engine, and still called
3147    // by pre-`encodeURIComponent` library code.
3148    "escape",
3149    "unescape",
3150    "eval",
3151    "String",
3152    "Number",
3153    "Boolean",
3154    "Array",
3155    "Object",
3156    "Function",
3157    "Symbol",
3158    "Map",
3159    "Set",
3160    "WeakMap",
3161    "WeakSet",
3162    "Promise",
3163    "Error",
3164    "TypeError",
3165    "RangeError",
3166    "SyntaxError",
3167    "ReferenceError",
3168    "EvalError",
3169    "URIError",
3170    "AggregateError",
3171    "BigInt",
3172    "RegExp",
3173    "Date",
3174    "ArrayBuffer",
3175    "Uint8Array",
3176    "Int8Array",
3177    "Uint8ClampedArray",
3178    "Int16Array",
3179    "Uint16Array",
3180    "Int32Array",
3181    "Uint32Array",
3182    "Float32Array",
3183    "Float64Array",
3184    "BigInt64Array",
3185    "BigUint64Array",
3186    "WeakRef",
3187    "FinalizationRegistry",
3188    "TextEncoder",
3189    "TextDecoder",
3190    // WHATWG Fetch globals (see `stdlib::fetch`).
3191    "fetch",
3192    "Headers",
3193    "Request",
3194    "Response",
3195    "Blob",
3196    "File",
3197    "FormData",
3198    "AbortController",
3199    "AbortSignal",
3200    "queueMicrotask",
3201    "setTimeout",
3202    "setInterval",
3203    "setImmediate",
3204    "clearTimeout",
3205    "clearInterval",
3206    "clearImmediate",
3207    "structuredClone",
3208    "Proxy",
3209    "require",
3210    // CommonJS loader dispatch targets referenced by per-module `require`
3211    // closures (see `module.rs`); never written by user code.
3212    "__cjs_require",
3213    "__cjs_resolve",
3214    "__cjs_cache",
3215];
3216
3217const NS_METHODS: &[&str] = &[
3218    "console.log",
3219    "console.error",
3220    "console.warn",
3221    "console.info",
3222    "console.debug",
3223    "Math.floor",
3224    "Math.ceil",
3225    "Math.round",
3226    "Math.trunc",
3227    "Math.abs",
3228    "Math.sign",
3229    "Math.max",
3230    "Math.min",
3231    "Math.pow",
3232    "Math.sqrt",
3233    "Math.cbrt",
3234    "Math.random",
3235    "Math.hypot",
3236    "Math.clz32",
3237    "Math.fround",
3238    "Math.imul",
3239    "Math.sinh",
3240    "Math.cosh",
3241    "Math.tanh",
3242    "Math.asinh",
3243    "Math.acosh",
3244    "Math.atanh",
3245    "Math.log1p",
3246    "Math.expm1",
3247    "Math.log",
3248    "Math.log2",
3249    "Math.log10",
3250    "Math.exp",
3251    "Math.sin",
3252    "Math.cos",
3253    "Math.tan",
3254    "Math.atan",
3255    "Math.atan2",
3256    "Math.asin",
3257    "Math.acos",
3258    "JSON.stringify",
3259    "JSON.parse",
3260    "Object.keys",
3261    "Object.values",
3262    "Object.entries",
3263    "Object.assign",
3264    "Object.freeze",
3265    "Object.is",
3266    "Object.fromEntries",
3267    "Object.getPrototypeOf",
3268    "Object.setPrototypeOf",
3269    "Object.create",
3270    "Object.getOwnPropertyNames",
3271    "Object.getOwnPropertySymbols",
3272    "Object.defineProperty",
3273    "Object.getOwnPropertyDescriptor",
3274    "Object.getOwnPropertyDescriptors",
3275    "Object.defineProperties",
3276    "Object.isFrozen",
3277    "Object.isSealed",
3278    "Object.seal",
3279    "Object.preventExtensions",
3280    "Object.isExtensible",
3281    "Object.hasOwn",
3282    "Object.groupBy",
3283    "Array.isArray",
3284    "Array.from",
3285    "Array.fromAsync",
3286    "Array.of",
3287    "Number.isInteger",
3288    "Number.isNaN",
3289    "Number.isFinite",
3290    "Number.isSafeInteger",
3291    "Number.parseInt",
3292    "Number.parseFloat",
3293    "String.fromCharCode",
3294    "String.fromCodePoint",
3295    "String.raw",
3296    "Symbol.for",
3297    "Symbol.keyFor",
3298    "BigInt.asIntN",
3299    "BigInt.asUintN",
3300    "Proxy.revocable",
3301    "Reflect.ownKeys",
3302    "Reflect.has",
3303    "Reflect.get",
3304    "Reflect.set",
3305    "Reflect.getPrototypeOf",
3306    "Reflect.setPrototypeOf",
3307    "Reflect.getOwnPropertyDescriptor",
3308    "Reflect.defineProperty",
3309    "Reflect.deleteProperty",
3310    "Reflect.apply",
3311    "Reflect.construct",
3312    "Reflect.isExtensible",
3313    "Reflect.preventExtensions",
3314    "Promise.resolve",
3315    "Promise.reject",
3316    "Promise.all",
3317    "Promise.allSettled",
3318    "Promise.race",
3319    "Promise.any",
3320    "Promise.withResolvers",
3321    "Map.groupBy",
3322    "Response.json",
3323    "Response.error",
3324    "Response.redirect",
3325    "AbortSignal.abort",
3326    "AbortSignal.timeout",
3327    "process.nextTick",
3328    "Error.captureStackTrace",
3329    "require.resolve",
3330];
3331
3332pub fn is_known_builtin(name: &str) -> bool {
3333    GLOBAL_FUNCS.contains(&name)
3334        || NS_METHODS.contains(&name)
3335        || is_namespace(name)
3336        || crate::stdlib::is_method(name)
3337}
3338
3339// ── dynamic functions (runtime source → callable) ────────────────────────────
3340
3341/// Build a callable from a complete function-expression source text — the ONE
3342/// dynamic-function generator on this frontend.
3343///
3344/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
3345/// wrapping parentheses needed to parse it as an expression: those are added
3346/// here, and `src` itself is retained so `Function.prototype.toString` reports
3347/// what V8 reports. The two callers synthesize different text and both shapes
3348/// are observable — see `stdlib::vm::compile_function` for the measured diff.
3349///
3350/// The body runs in the MODULE scope, never the constructing function's scope
3351/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
3352/// *global* environment). That also makes a `var` inside the body a function
3353/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
3354/// returns 6 and leaves `globalThis.zz` `undefined`.
3355pub fn dynamic_function(src: &str) -> Result<Value, String> {
3356    let f = crate::eval_in_global_scope(&format!("({src})"))?;
3357    with_host(|h| {
3358        let s = h.new_str(src.to_string());
3359        h.set_fn_prop(&f, "@@source", s);
3360    });
3361    Ok(f)
3362}
3363
3364/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
3365///
3366/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
3367/// are parameter-list fragments joined with `,` — so a fragment may itself hold
3368/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
3369/// arguments at all, both the parameter list and the body are empty.
3370///
3371/// Measured on node v26.7.0:
3372///
3373/// ```text
3374/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
3375/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
3376/// new Function('a,b','c','return [a,b,c]').length === 3
3377/// new Function('a','b','return a+b').name       === 'anonymous'
3378/// ```
3379pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
3380    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
3381    let (params, body) = match parts.split_last() {
3382        Some((body, params)) => (params.join(","), body.clone()),
3383        None => (String::new(), String::new()),
3384    };
3385    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
3386}
3387
3388/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
3389/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
3390/// other route to the same function value is an INDIRECT eval and evaluates in
3391/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
3392/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
3393/// do not.
3394///
3395/// A non-string argument is returned unchanged (19.2.1.1 step 2).
3396pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
3397    let v = arg.cloned().unwrap_or(Value::Undef);
3398    let is_string =
3399        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
3400    if !is_string {
3401        return Ok(v);
3402    }
3403    let src = with_host(|h| h.str_of(&v));
3404    let chunk = crate::load_merged(crate::compile_completion(&src)?);
3405    if direct {
3406        host::run_chunk_on(chunk)
3407    } else {
3408        host::run_chunk_in_global_scope(chunk)
3409    }
3410}
3411
3412/// Call a resolved builtin function (global or `namespace.method`).
3413pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
3414    // `require(spec)`: the ENTRY script's top-level require — core module first,
3415    // else the CommonJS loader resolving from the entry file's directory.
3416    if name == "require" {
3417        let spec = with_host(|h| h.str_of(&arg0(&args)));
3418        return crate::module::require(&spec, &crate::module::entry_dir());
3419    }
3420    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
3421    // into the loader, resolving `spec` against the module's own directory.
3422    if name == "__cjs_require" {
3423        let spec = with_host(|h| h.str_of(&arg0(&args)));
3424        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
3425        return crate::module::require(&spec, std::path::Path::new(&from));
3426    }
3427    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
3428    if name == "require.resolve" {
3429        let spec = with_host(|h| h.str_of(&arg0(&args)));
3430        if crate::stdlib::resolve(&spec).is_some() {
3431            return Ok(with_host(|h| h.new_str(spec)));
3432        }
3433        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
3434            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
3435            None => Err(crate::host::plain_coded_error(
3436                "Error",
3437                "MODULE_NOT_FOUND",
3438                &format!("Cannot find module '{spec}'"),
3439            )),
3440        };
3441    }
3442    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
3443    // path (core modules resolve to the bare specifier, as in Node).
3444    if name == "__cjs_resolve" {
3445        let spec = with_host(|h| h.str_of(&arg0(&args)));
3446        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
3447        if crate::stdlib::resolve(&spec).is_some() {
3448            return Ok(with_host(|h| h.new_str(spec)));
3449        }
3450        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
3451            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
3452            None => Err(crate::host::plain_coded_error(
3453                "Error",
3454                "MODULE_NOT_FOUND",
3455                &format!("Cannot find module '{spec}'"),
3456            )),
3457        };
3458    }
3459    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
3460    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
3461    // stack-introspection pattern used by `depd`), it is called with a synthetic
3462    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
3463    if name == "Error.captureStackTrace" {
3464        let target = arg0(&args);
3465        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
3466        let stack = match prep {
3467            Some(f)
3468                if matches!(
3469                    with_host(|h| h.get(&f).cloned()),
3470                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
3471                ) =>
3472            {
3473                let sites = crate::module::callsite_stack(10)?;
3474                host::invoke(&f, vec![target.clone(), sites], None)?
3475            }
3476            _ => with_host(|h| h.new_str("")),
3477        };
3478        let _ = set_property(&target, "stack", stack);
3479        return Ok(Value::Undef);
3480    }
3481    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
3482    if let Some(r) = crate::stdlib::call(name, &args) {
3483        return r;
3484    }
3485    match name {
3486        "console.log" | "console.info" | "console.debug" => {
3487            print_line(&args, false);
3488            Ok(Value::Undef)
3489        }
3490        "console.error" | "console.warn" => {
3491            print_line(&args, true);
3492            Ok(Value::Undef)
3493        }
3494        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args))),
3495        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args))),
3496        "isNaN" => Ok(Value::Bool(arg_num(&args, 0).is_nan())),
3497        "isFinite" => Ok(Value::Bool(arg_num(&args, 0).is_finite())),
3498        "encodeURIComponent" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), false),
3499        "encodeURI" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), true),
3500        "decodeURIComponent" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), false),
3501        "decodeURI" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), true),
3502        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
3503        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
3504        // Reaching `eval` through this table means the eval FUNCTION VALUE was
3505        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
3506        // Those are INDIRECT evals and run in the global scope. A literal
3507        // `eval(src)` is intercepted earlier, in `host::call_named`.
3508        "eval" => eval_source(args.first(), false),
3509        // `new Function(...)` and `Function(...)` are the same operation
3510        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
3511        // [[Construct]]), so both route to the one generator.
3512        "Function" => function_ctor(&args),
3513        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
3514        // (DEP0005). Node still supports it and still routes it to the same place
3515        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
3516        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
3517        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
3518        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
3519        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
3520        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
3521        // where Node warns.
3522        "Buffer" => {
3523            crate::stdlib::process::emit_deprecation_warning(
3524                "DEP0005",
3525                "Buffer() is deprecated due to security and usability issues. \
3526                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
3527                 Buffer.from() methods instead.",
3528            );
3529            crate::stdlib::construct("Buffer", &args)
3530                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
3531        }
3532        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
3533        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
3534        "Number.isNaN" => Ok(Value::Bool(
3535            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
3536        )),
3537        "Number.isFinite" => Ok(Value::Bool(
3538            matches!(arg0(&args), Value::Float(f) if f.is_finite())
3539                || matches!(arg0(&args), Value::Int(_)),
3540        )),
3541        "String" => {
3542            if args.is_empty() {
3543                Ok(with_host(|h| h.new_str("")))
3544            } else {
3545                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
3546                // is allowed); everything else via ToString method dispatch.
3547                host::string_ctor_value(&args[0])
3548            }
3549        }
3550        "Number" => Ok(Value::Float(if args.is_empty() {
3551            0.0
3552        } else {
3553            // ToNumber, which for an object runs ToPrimitive (a JS `valueOf` call).
3554            host::to_number_value(&args[0])?
3555        })),
3556        "BigInt" => bigint_ctor(&arg0(&args)),
3557        "RegExp" => regexp_ctor(&args),
3558        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
3559        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
3560        // Each argument is truncated to a uint16 and taken as one code UNIT, so
3561        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
3562        // a surrogate PAIR of arguments composes into one character.
3563        "String.fromCharCode" => Ok(with_host(|h| {
3564            let units: Vec<u16> = args
3565                .iter()
3566                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
3567                .collect();
3568            let s = crate::utf16::to_string_lossy(&units);
3569            h.new_str(s)
3570        })),
3571        // `fromCodePoint` takes whole code POINTS and rejects anything that is
3572        // not one — including a lone surrogate, which `fromCharCode` accepts.
3573        "String.fromCodePoint" => {
3574            let mut s = String::new();
3575            for a in &args {
3576                let n = with_host(|h| h.to_number(a));
3577                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
3578                {
3579                    char::from_u32(n as u32)
3580                } else {
3581                    None
3582                };
3583                match cp {
3584                    Some(c) => s.push(c),
3585                    None => {
3586                        return Err(format!(
3587                            "RangeError: Invalid code point {}",
3588                            with_host(|h| h.str_of(a))
3589                        ))
3590                    }
3591                }
3592            }
3593            Ok(new_s(s))
3594        }
3595        "String.raw" => string_raw(&args),
3596        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
3597        "Array" => construct_builtin("Array", args),
3598        "Array.of" => Ok(with_host(|h| h.new_array(args))),
3599        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
3600        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
3601        "Array.isArray" => {
3602            let v = arg0(&args);
3603            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
3604            Ok(Value::Bool(matches!(
3605                with_host(|h| h.get(&subject).cloned()),
3606                Some(JsObj::Array(_))
3607            )))
3608        }
3609        "Array.from" => array_from(args),
3610        "Array.fromAsync" => array_from_async(args),
3611        "Object" => Ok(object_call(args)),
3612        "Object.keys" => object_keys(args, 0),
3613        "Object.values" => object_keys(args, 1),
3614        "Object.entries" => object_keys(args, 2),
3615        "Object.assign" => object_assign(args),
3616        "Object.freeze" => {
3617            let v = arg0(&args);
3618            with_host(|h| h.seal_object(&v, true));
3619            Ok(v)
3620        }
3621        "Object.seal" => {
3622            let v = arg0(&args);
3623            with_host(|h| h.seal_object(&v, false));
3624            Ok(v)
3625        }
3626        "Object.preventExtensions" => {
3627            let v = arg0(&args);
3628            if crate::proxy::prevent_extensions(&v)? {
3629                return Ok(v);
3630            }
3631            with_host(|h| h.prevent_extensions(&v));
3632            Ok(v)
3633        }
3634        "Object.isFrozen" => Ok(Value::Bool(with_host(|h| h.is_sealed(&arg0(&args), true)))),
3635        "Object.isSealed" => Ok(Value::Bool(with_host(|h| h.is_sealed(&arg0(&args), false)))),
3636        "Object.isExtensible" => {
3637            let v = arg0(&args);
3638            match crate::proxy::is_extensible(&v)? {
3639                Some(b) => Ok(Value::Bool(b)),
3640                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
3641            }
3642        }
3643        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
3644        // distinct from -0.
3645        "Object.is" => {
3646            let a = arg0(&args);
3647            let b = args.get(1).cloned().unwrap_or(Value::Undef);
3648            let num = |v: &Value| match v {
3649                Value::Int(n) => Some(*n as f64),
3650                Value::Float(f) => Some(*f),
3651                _ => None,
3652            };
3653            let r = match (num(&a), num(&b)) {
3654                (Some(x), Some(y)) => {
3655                    if x.is_nan() && y.is_nan() {
3656                        true
3657                    } else if x == 0.0 && y == 0.0 {
3658                        x.is_sign_negative() == y.is_sign_negative()
3659                    } else {
3660                        x == y
3661                    }
3662                }
3663                _ => with_host(|h| h.strict_eq(&a, &b)),
3664            };
3665            Ok(Value::Bool(r))
3666        }
3667        "Object.fromEntries" => object_from_entries(args),
3668        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
3669        // so the proxy form cannot share `prototype_of`'s infallible signature.
3670        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
3671            let v = arg0(&args);
3672            match crate::proxy::get_prototype_of(&v)? {
3673                Some(p) => Ok(p),
3674                None => Ok(prototype_of(&v)),
3675            }
3676        }
3677        "Object.setPrototypeOf" => {
3678            let obj = arg0(&args);
3679            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
3680            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
3681                reject_bad_prototype(&proto)?;
3682                crate::proxy::set_prototype_of(&obj, &proto)?;
3683                return Ok(obj);
3684            }
3685            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
3686            // prototype type check, then — only for an actual object target —
3687            // the extensibility check. A PRIMITIVE target is returned untouched
3688            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
3689            // extensibility test cannot come first.
3690            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
3691                return Err(host::type_error(
3692                    "Object.setPrototypeOf called on null or undefined",
3693                ));
3694            }
3695            reject_bad_prototype(&proto)?;
3696            if with_host(|h| is_object_like(h, &obj)) {
3697                // Setting the SAME prototype is a no-op and stays legal even on a
3698                // frozen object: node v26.7.0 accepts
3699                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
3700                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
3701                // link still has `Object.prototype`, and comparing against the
3702                // absent link would call that a change.
3703                let cur = prototype_of(&obj);
3704                let same = with_host(|h| h.strict_eq(&cur, &proto));
3705                if !same && !with_host(|h| h.is_extensible(&obj)) {
3706                    return Err(host::type_error("#<Object> is not extensible"));
3707                }
3708                with_host(|h| h.set_proto(&obj, proto));
3709            }
3710            Ok(obj)
3711        }
3712        "Object.create" => object_create(args),
3713        "Object.getOwnPropertyNames" => object_keys(args, 3),
3714        "Object.getOwnPropertySymbols" => {
3715            let v = arg0(&args);
3716            require_object_coercible(&v)?;
3717            let syms = proxy_or_own_symbol_keys(&v)?;
3718            Ok(with_host(|h| h.new_array(syms)))
3719        }
3720        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
3721        "Object.hasOwn" => {
3722            let obj = arg0(&args);
3723            let key = args.get(1).cloned().unwrap_or(Value::Undef);
3724            object_builtin_method(&obj, "hasOwnProperty", vec![key])
3725        }
3726        "Object.defineProperty" => object_define_property(args),
3727        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
3728        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
3729        "Object.defineProperties" => object_define_properties(args),
3730        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
3731        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
3732        "Object.groupBy" => object_group_by(args),
3733        "Symbol" => Ok(with_host(|h| {
3734            let desc = args
3735                .first()
3736                .filter(|a| !matches!(a, Value::Undef))
3737                .map(|a| h.str_of(a));
3738            h.new_symbol(desc)
3739        })),
3740        "Symbol.for" => Ok(with_host(|h| {
3741            let key = h.str_of(&arg0(&args));
3742            h.symbol_for(&key)
3743        })),
3744        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
3745        // description read: it answers only for symbols `Symbol.for` created.
3746        // Returning the description made every symbol look registered —
3747        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
3748        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
3749        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
3750        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
3751        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
3752        "Proxy.revocable" => crate::proxy::revocable(&args),
3753        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
3754        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
3755        // own properties, so there is no second half to append).
3756        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
3757        // non-enumerable included, strings first and then the SYMBOLS.
3758        "Reflect.ownKeys" => {
3759            let v = arg0(&args);
3760            let names = object_keys(args, 3)?;
3761            let syms = proxy_or_own_symbol_keys(&v)?;
3762            if syms.is_empty() {
3763                return Ok(names);
3764            }
3765            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
3766            all.extend(syms);
3767            Ok(with_host(|h| h.new_array(all)))
3768        }
3769        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
3770        "Reflect.defineProperty" => {
3771            object_define_property(args)?;
3772            Ok(Value::Bool(true))
3773        }
3774        "Reflect.deleteProperty" => {
3775            let obj = arg0(&args);
3776            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3777            Ok(Value::Bool(delete_property(&obj, &k)?))
3778        }
3779        "Reflect.setPrototypeOf" => {
3780            let obj = arg0(&args);
3781            let p = args.get(1).cloned().unwrap_or(Value::Undef);
3782            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
3783                crate::proxy::set_prototype_of(&obj, &p)?;
3784                return Ok(Value::Bool(true));
3785            }
3786            with_host(|h| h.set_proto(&obj, p));
3787            Ok(Value::Bool(true))
3788        }
3789        "Reflect.isExtensible" => {
3790            let v = arg0(&args);
3791            match crate::proxy::is_extensible(&v)? {
3792                Some(b) => Ok(Value::Bool(b)),
3793                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
3794            }
3795        }
3796        "Reflect.preventExtensions" => {
3797            let v = arg0(&args);
3798            if crate::proxy::prevent_extensions(&v)? {
3799                return Ok(Value::Bool(true));
3800            }
3801            with_host(|h| h.prevent_extensions(&v));
3802            Ok(Value::Bool(true))
3803        }
3804        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
3805        "Reflect.apply" => {
3806            let f = arg0(&args);
3807            let this = args.get(1).cloned();
3808            let list = with_host(|h| h.iter_vec(&args.get(2).cloned().unwrap_or(Value::Undef)))
3809                .unwrap_or_default();
3810            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
3811        }
3812        "Reflect.construct" => {
3813            let f = arg0(&args);
3814            let list = with_host(|h| h.iter_vec(&args.get(1).cloned().unwrap_or(Value::Undef)))
3815                .unwrap_or_default();
3816            host::construct(&f, list)
3817        }
3818        "Reflect.has" => {
3819            let obj = arg0(&args);
3820            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3821            Ok(Value::Bool(has_property(&obj, &k)?))
3822        }
3823        // `Reflect.get(target, key, receiver)` — the optional third argument is
3824        // what a getter sees as `this` (28.1.6). Defaults to the target.
3825        "Reflect.get" => {
3826            let obj = arg0(&args);
3827            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3828            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
3829            get_property_recv(&obj, &k, &receiver)
3830        }
3831        "Reflect.set" => {
3832            let obj = arg0(&args);
3833            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3834            let v = args.get(2).cloned().unwrap_or(Value::Undef);
3835            let _ = set_property(&obj, &k, v);
3836            Ok(Value::Bool(true))
3837        }
3838        "JSON.stringify" => json_stringify(args),
3839        "JSON.parse" => json_parse(args),
3840        "structuredClone" => Ok(deep_clone(&arg0(&args))),
3841        "fetch" => crate::stdlib::fetch::fetch(&args),
3842        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
3843        // suffix is the signal's heap index.
3844        _ if name.starts_with("@@aborttimeout:") => {
3845            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
3846            crate::stdlib::fetch::fire_timeout_abort(idx)
3847        }
3848        "queueMicrotask" | "process.nextTick" => {
3849            let cb = arg0(&args);
3850            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
3851            enqueue_microtask(name == "process.nextTick", cb, rest);
3852            Ok(Value::Undef)
3853        }
3854        "setTimeout" | "setInterval" | "setImmediate" => Ok(schedule_timer(name, args)),
3855        "clearTimeout" | "clearInterval" | "clearImmediate" => {
3856            clear_timer(&arg0(&args));
3857            Ok(Value::Undef)
3858        }
3859        "Promise.resolve" => promise_resolve(arg0(&args)),
3860        "Promise.reject" => promise_reject(arg0(&args)),
3861        "Promise.all" => promise_all(args, AllMode::All),
3862        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
3863        "Promise.race" => promise_race(args, false),
3864        "Promise.any" => promise_race(args, true),
3865        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
3866        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
3867        "Promise.withResolvers" => promise_with_resolvers(),
3868        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
3869        // `cb(item, i)` result (SameValueZero), each value an array of members.
3870        "Map.groupBy" => map_group_by(args),
3871        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
3872        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
3873        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
3874        _ if name.starts_with("@@presolve:") => {
3875            let id: u32 = name[11..].parse().unwrap_or(0);
3876            host::resolve_promise_val(id, arg0(&args));
3877            Ok(Value::Undef)
3878        }
3879        _ if name.starts_with("@@preject:") => {
3880            let id: u32 = name[10..].parse().unwrap_or(0);
3881            host::reject_promise_val(id, arg0(&args));
3882            Ok(Value::Undef)
3883        }
3884        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
3885        // index so calling it twice is the spec's no-op rather than a re-tear.
3886        _ if name.starts_with("@@prevoke:") => {
3887            let i: u32 = name[10..].parse().unwrap_or(0);
3888            Ok(crate::proxy::revoke(i))
3889        }
3890        _ if name.starts_with("@@finpass:") => {
3891            // finally(cb) on fulfill: run cb, then pass the value through.
3892            let i: u32 = name[10..].parse().unwrap_or(0);
3893            let cb = Value::Obj(i);
3894            host::invoke(&cb, Vec::new(), None)?;
3895            Ok(arg0(&args))
3896        }
3897        _ if name.starts_with("@@finthrow:") => {
3898            // finally(cb) on reject: run cb, then re-throw the reason.
3899            let i: u32 = name[11..].parse().unwrap_or(0);
3900            let cb = Value::Obj(i);
3901            host::invoke(&cb, Vec::new(), None)?;
3902            let reason = arg0(&args);
3903            with_host(|h| h.exc = Some(reason.clone()));
3904            Err(with_host(|h| error_string(h, &reason)))
3905        }
3906        _ => Err(host::type_error(&format!("{name} is not a function"))),
3907    }
3908}
3909
3910/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
3911/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
3912/// (matching Node's messages).
3913/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
3914/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
3915/// BigInt`. The old text said "value" literally, for every input.
3916fn bigint_convert_error(v: &Value) -> String {
3917    let shown = with_host(|h| h.str_of(v));
3918    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
3919}
3920
3921fn bigint_ctor(v: &Value) -> Result<Value, String> {
3922    use num_bigint::BigInt;
3923    let big = match v {
3924        Value::Bool(b) => BigInt::from(*b as i64),
3925        Value::Int(n) => BigInt::from(*n),
3926        Value::Float(f) => {
3927            if !f.is_finite() || f.fract() != 0.0 {
3928                let disp = with_host(|h| h.str_of(v));
3929                return Err(format!(
3930                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
3931                ));
3932            }
3933            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
3934            // .toString` switches to exponential notation at 1e21, and
3935            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
3936            // threw `Cannot convert value to a BigInt` where node returns
3937            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
3938            // value, which is also what node reports for a magnitude past the
3939            // exactly-representable range (`BigInt(1e30)` is
3940            // `1000000000000000019884624838656n` in both).
3941            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
3942                Some(b) => b,
3943                None => return Err(bigint_convert_error(v)),
3944            }
3945        }
3946        Value::Str(s) => match host::parse_bigint_str(s) {
3947            Some(b) => b,
3948            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
3949        },
3950        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
3951            Some(JsObj::BigInt(b)) => b,
3952            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
3953                Some(b) => b,
3954                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
3955            },
3956            _ => return Err(bigint_convert_error(v)),
3957        },
3958        _ => return Err(bigint_convert_error(v)),
3959    };
3960    Ok(with_host(|h| h.new_bigint(big)))
3961}
3962
3963/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
3964/// its source (and flags, unless new ones are given).
3965fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
3966    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
3967        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
3968        _ => {
3969            let a0 = arg0(args);
3970            let src = if matches!(a0, Value::Undef) {
3971                String::new()
3972            } else {
3973                with_host(|h| h.str_of(&a0))
3974            };
3975            (src, None)
3976        }
3977    };
3978    let flags = match args.get(1) {
3979        Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
3980        _ => existing_flags.unwrap_or_default(),
3981    };
3982    // An empty source compiles as the JS canonical `(?:)`.
3983    let src = if source.is_empty() {
3984        "(?:)".to_string()
3985    } else {
3986        source
3987    };
3988    crate::regexp::build_regexp(&src, &flags)
3989}
3990
3991/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
3992/// two's-complement (signed) or unsigned integer.
3993fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
3994    use num_bigint::BigInt;
3995    use num_traits::Signed;
3996    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
3997    if bits < 0 {
3998        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
3999    }
4000    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
4001        Some(b) => b,
4002        None => return Err(host::type_error("Cannot convert to a BigInt")),
4003    };
4004    let bits = bits as u32;
4005    if bits == 0 {
4006        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
4007    }
4008    let modulus = BigInt::from(1) << bits; // 2^bits
4009                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
4010    let mut r = &x % &modulus;
4011    if r.is_negative() {
4012        r += &modulus;
4013    }
4014    if !unsigned {
4015        let half = BigInt::from(1) << (bits - 1);
4016        if r >= half {
4017            r -= &modulus;
4018        }
4019    }
4020    Ok(with_host(|h| h.new_bigint(r)))
4021}
4022
4023/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
4024/// interleaved with the substitutions.
4025fn string_raw(args: &[Value]) -> Result<Value, String> {
4026    let call_site = arg0(args);
4027    let raw = get_property(&call_site, "raw")?;
4028    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
4029    let mut out = String::new();
4030    for (i, r) in raws.iter().enumerate() {
4031        out.push_str(&with_host(|h| h.str_of(r)));
4032        if i + 1 < raws.len() {
4033            if let Some(sub) = args.get(i + 1) {
4034                out.push_str(&with_host(|h| h.str_of(sub)));
4035            }
4036        }
4037    }
4038    Ok(with_host(|h| h.new_str(out)))
4039}
4040
4041/// `Object(x)`: box/pass-through — for our model, non-object args just return a
4042/// fresh object; objects pass through.
4043fn object_call(args: Vec<Value>) -> Value {
4044    let a = arg0(&args);
4045    if matches!(
4046        with_host(|h| h.get(&a).cloned()),
4047        Some(JsObj::Object(_)) | Some(JsObj::Array(_))
4048    ) {
4049        a
4050    } else {
4051        with_host(|h| h.new_object(IndexMap::new()))
4052    }
4053}
4054
4055/// Construct via `new` for the builtin constructors.
4056pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
4057    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
4058    if let Some(r) = crate::stdlib::construct(name, &args) {
4059        return r;
4060    }
4061    match name {
4062        "Array" => {
4063            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
4064            // A single NUMBER argument is a length and is validated as one
4065            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
4066            // `new Array(2**32)` are all `RangeError: Invalid array length` on
4067            // node v26.7.0; only a non-number single argument is an element.
4068            if args.len() == 1 {
4069                if let Value::Float(_) | Value::Int(_) = args[0] {
4070                    let n = host::to_array_length(&args[0])?;
4071                    // Every element of `new Array(n)` is a HOLE, not a stored
4072                    // `undefined`: `Object.keys(Array(3))` is `[]`.
4073                    return Ok(with_host(|h| {
4074                        let a = h.new_array(vec![Value::Undef; n]);
4075                        h.mark_hole_range(&a, 0..n);
4076                        a
4077                    }));
4078                }
4079            }
4080            Ok(with_host(|h| h.new_array(args)))
4081        }
4082        "Object" => Ok(object_call(args)),
4083        "Map" | "WeakMap" => {
4084            let weak = name == "WeakMap";
4085            let m = with_host(|h| {
4086                h.alloc(JsObj::Map {
4087                    entries: indexmap::IndexMap::new(),
4088                    weak,
4089                })
4090            });
4091            if let Some(init) = args
4092                .first()
4093                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
4094            {
4095                let pairs = host::iter_all(init)?;
4096                for p in pairs {
4097                    let kv = host::iter_all(&p)?;
4098                    let k = kv.first().cloned().unwrap_or(Value::Undef);
4099                    let v = kv.get(1).cloned().unwrap_or(Value::Undef);
4100                    map_method(&m, "set", vec![k, v])?;
4101                }
4102            }
4103            Ok(m)
4104        }
4105        "Set" | "WeakSet" => {
4106            let weak = name == "WeakSet";
4107            let s = with_host(|h| {
4108                h.alloc(JsObj::Set {
4109                    entries: indexmap::IndexMap::new(),
4110                    weak,
4111                })
4112            });
4113            if let Some(init) = args
4114                .first()
4115                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
4116            {
4117                let vals = host::iter_all(init)?;
4118                for v in vals {
4119                    set_method(&s, "add", vec![v])?;
4120                }
4121            }
4122            Ok(s)
4123        }
4124        "Promise" => new_promise(arg0(&args)),
4125        "Proxy" => crate::proxy::create(&args),
4126        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
4127        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
4128        // deprecation wrapper this way, so `require('body-parser')` — and with it
4129        // `require('express')` — dies at load without it.
4130        "Function" => function_ctor(&args),
4131        "RegExp" => regexp_ctor(&args),
4132        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
4133        "Error" => Ok(make_error(name, &args)),
4134        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
4135        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
4136    }
4137}
4138
4139fn make_error(name: &str, args: &[Value]) -> Value {
4140    // `new AggregateError(errors, message)` takes the causes FIRST; every other
4141    // error constructor takes the message first.
4142    let agg = name == "AggregateError";
4143    let (errors, args) = if agg {
4144        (
4145            Some(args.first().cloned().unwrap_or(Value::Undef)),
4146            args.get(1..).unwrap_or(&[]),
4147        )
4148    } else {
4149        (None, args)
4150    };
4151    with_host(|h| {
4152        h.ensure_error_protos();
4153        let mut props: IndexMap<String, Value> = IndexMap::new();
4154        let msg = args
4155            .first()
4156            .filter(|a| !matches!(a, Value::Undef))
4157            .map(|a| h.str_of(a));
4158        if let Some(m) = &msg {
4159            let mv = h.new_str(m.clone());
4160            props.insert("message".into(), mv);
4161        }
4162        // `.stack` is engine-specific; a simple `Name: message` header line
4163        // suffices for parity (the fuzzer never prints raw stacks).
4164        let frames = h.stack_frames();
4165        let stack = match &msg {
4166            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
4167            _ => format!("{name}{frames}"),
4168        };
4169        let sv = h.new_str(stack);
4170        props.insert("stack".into(), sv);
4171        if let Some(errs) = errors {
4172            // Materialize the iterable into the own `errors` array property.
4173            let items = h.iter_vec(&errs).unwrap_or_default();
4174            let arr = h.new_array(items);
4175            props.insert("errors".into(), arr);
4176        }
4177        // `new Error(msg, { cause })` (ES2022): installed only when the options
4178        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
4179        let opts = args.get(1);
4180        if let Some(cause) = opts.and_then(|o| match h.get(o) {
4181            Some(JsObj::Object(p)) => p.get("cause").cloned(),
4182            _ => None,
4183        }) {
4184            props.insert("cause".into(), cause);
4185        }
4186        let e = h.new_object(props);
4187        if let Some(p) = host::error_proto_of(h, name) {
4188            h.set_proto(&e, p);
4189        }
4190        // Every own slot an error constructor installs is non-enumerable in V8,
4191        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
4192        // `{}` — properties a *script* later assigns stay enumerable.
4193        for k in ["message", "stack", "errors", "cause"] {
4194            h.hide_prop(&e, k);
4195        }
4196        e
4197    })
4198}
4199
4200fn print_line(args: &[Value], stderr: bool) {
4201    // Node's console.log(...args) === util.format(...args): printf-style
4202    // substitution when the first arg is a format string, else inspect-and-join.
4203    let line: String = crate::stdlib::util::format(args);
4204    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
4205}
4206
4207fn arg0(args: &[Value]) -> Value {
4208    args.first().cloned().unwrap_or(Value::Undef)
4209}
4210fn arg_num(args: &[Value], i: usize) -> f64 {
4211    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
4212}
4213
4214fn is_integer(v: Value) -> bool {
4215    match v {
4216        Value::Int(_) => true,
4217        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
4218        _ => false,
4219    }
4220}
4221fn is_safe_integer(v: Value) -> bool {
4222    match v {
4223        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
4224        Value::Int(_) => true,
4225        _ => false,
4226    }
4227}
4228
4229/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
4230/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
4231/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
4232fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
4233    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
4234    const UNRESERVED: &[u8] =
4235        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
4236    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
4237    const RESERVED: &[u8] = b";,/?:@&=+$#";
4238    let mut out = String::with_capacity(s.len());
4239    for &b in s.as_bytes() {
4240        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
4241            out.push(b as char);
4242        } else {
4243            out.push('%');
4244            out.push(
4245                char::from_digit((b >> 4) as u32, 16)
4246                    .unwrap()
4247                    .to_ascii_uppercase(),
4248            );
4249            out.push(
4250                char::from_digit((b & 0xf) as u32, 16)
4251                    .unwrap()
4252                    .to_ascii_uppercase(),
4253            );
4254        }
4255    }
4256    Ok(with_host(|h| h.new_str(out)))
4257}
4258
4259/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
4260/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
4261/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
4262fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
4263    const RESERVED: &[u8] = b";,/?:@&=+$#";
4264    let bytes = s.as_bytes();
4265    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
4266    let mut i = 0;
4267    while i < bytes.len() {
4268        if bytes[i] == b'%' {
4269            if i + 2 >= bytes.len() {
4270                return Err("URIError: URI malformed".into());
4271            }
4272            let hi = (bytes[i + 1] as char).to_digit(16);
4273            let lo = (bytes[i + 2] as char).to_digit(16);
4274            match (hi, lo) {
4275                (Some(h), Some(l)) => {
4276                    let byte = (h * 16 + l) as u8;
4277                    // decodeURI keeps reserved-delimiter escapes literal.
4278                    if uri && RESERVED.contains(&byte) {
4279                        out.extend_from_slice(&bytes[i..i + 3]);
4280                    } else {
4281                        out.push(byte);
4282                    }
4283                    i += 3;
4284                }
4285                _ => return Err("URIError: URI malformed".into()),
4286            }
4287        } else {
4288            out.push(bytes[i]);
4289            i += 1;
4290        }
4291    }
4292    match String::from_utf8(out) {
4293        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
4294        Err(_) => Err("URIError: URI malformed".into()),
4295    }
4296}
4297
4298/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
4299/// present in every engine and still reached by old libraries (jQuery's cookie
4300/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
4301/// bytes, which is what separates it from `encodeURIComponent`: a unit below
4302/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
4303/// character yields the two escapes of its surrogate pair
4304/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
4305///
4306/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
4307/// it keeps `@*_+-./` and drops `!~'()`.
4308fn legacy_escape(s: &str) -> Result<Value, String> {
4309    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
4310    let mut out = String::with_capacity(s.len());
4311    for u in s.encode_utf16() {
4312        if u < 0x100 {
4313            if KEEP.contains(&(u as u8)) {
4314                out.push(u as u8 as char);
4315            } else {
4316                out.push_str(&format!("%{u:02X}"));
4317            }
4318        } else {
4319            out.push_str(&format!("%u{u:04X}"));
4320        }
4321    }
4322    Ok(with_host(|h| h.new_str(out)))
4323}
4324
4325/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
4326/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
4327/// `%XX` or `%uXXXX` escape is passed through literally
4328/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
4329///
4330/// Decoding is done in code-unit space and re-joined at the end so a
4331/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
4332fn legacy_unescape(s: &str) -> Result<Value, String> {
4333    let b = s.as_bytes();
4334    let hex = |i: usize, n: usize| -> Option<u16> {
4335        if i + n > b.len() {
4336            return None;
4337        }
4338        let mut v: u16 = 0;
4339        for &c in &b[i..i + n] {
4340            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
4341        }
4342        Some(v)
4343    };
4344    let units: Vec<u16> = s.encode_utf16().collect();
4345    let mut out: Vec<u16> = Vec::with_capacity(units.len());
4346    let mut i = 0;
4347    while i < b.len() {
4348        // Escapes are pure ASCII, so a byte index is a unit index up to here —
4349        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
4350        if b[i] == b'%' {
4351            if let Some(u) = hex(i + 1, 2) {
4352                out.push(u);
4353                i += 3;
4354                continue;
4355            }
4356            if b.get(i + 1) == Some(&b'u') {
4357                if let Some(u) = hex(i + 2, 4) {
4358                    out.push(u);
4359                    i += 6;
4360                    continue;
4361                }
4362            }
4363        }
4364        let c = s[i..].chars().next().unwrap_or('%');
4365        let mut buf = [0u16; 2];
4366        out.extend_from_slice(c.encode_utf16(&mut buf));
4367        i += c.len_utf8();
4368    }
4369    Ok(with_host(|h| {
4370        h.new_str(crate::utf16::to_string_lossy(&out))
4371    }))
4372}
4373
4374fn parse_int(args: &[Value]) -> f64 {
4375    let s = with_host(|h| h.str_of(&arg0(args)));
4376    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
4377    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
4378    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
4379    let radix_arg = args
4380        .get(1)
4381        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
4382    let radix = match radix_arg {
4383        Some(0) | None => None,
4384        Some(r) if (2..=36).contains(&r) => Some(r as u32),
4385        Some(_) => return f64::NAN,
4386    };
4387    let t = crate::utf16::js_trim_start(&s);
4388    let (neg, digits) = match t.strip_prefix('-') {
4389        Some(rest) => (true, rest),
4390        None => (false, t.strip_prefix('+').unwrap_or(t)),
4391    };
4392    let (radix, digits) = match radix {
4393        Some(16) => (
4394            16u32,
4395            digits
4396                .strip_prefix("0x")
4397                .or_else(|| digits.strip_prefix("0X"))
4398                .unwrap_or(digits),
4399        ),
4400        Some(r) => (r, digits),
4401        None => {
4402            if let Some(hex) = digits
4403                .strip_prefix("0x")
4404                .or_else(|| digits.strip_prefix("0X"))
4405            {
4406                (16, hex)
4407            } else {
4408                (10, digits)
4409            }
4410        }
4411    };
4412    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
4413    if valid.is_empty() {
4414        return f64::NAN;
4415    }
4416    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
4417    // digits and the error was mapped to `NaN`, so
4418    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
4419    // asks for the mathematical value rounded to a Number, which is what
4420    // repeated multiply-accumulate in `f64` produces.
4421    let n = if radix == 10 {
4422        // Rust's decimal float parser is correctly rounded; digit-by-digit
4423        // multiply-accumulate is not, and drifted a ULP on long inputs
4424        // (`parseInt("999999999999999999999999")` came out
4425        // 1.0000000000000003e+24 rather than 1e+24).
4426        valid.parse::<f64>().unwrap_or(f64::NAN)
4427    } else {
4428        let mut n = 0.0f64;
4429        for c in valid.chars() {
4430            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
4431        }
4432        n
4433    };
4434    if neg {
4435        -n
4436    } else {
4437        n
4438    }
4439}
4440
4441fn parse_float(args: &[Value]) -> f64 {
4442    let s = with_host(|h| h.str_of(&arg0(args)));
4443    let t = crate::utf16::js_trim_start(&s);
4444    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
4445    let inf_body = t
4446        .strip_prefix('+')
4447        .or_else(|| t.strip_prefix('-'))
4448        .unwrap_or(t);
4449    if inf_body.starts_with("Infinity") {
4450        return if t.starts_with('-') {
4451            f64::NEG_INFINITY
4452        } else {
4453            f64::INFINITY
4454        };
4455    }
4456    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
4457    // not the same as the longest run of characters that could appear in one:
4458    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
4459    // only valid once a digit follows `e`. Tracking `end` at every character
4460    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
4461    // came back NaN.
4462    let mut end = 0;
4463    let bytes = t.as_bytes();
4464    let mut seen_dot = false;
4465    let mut seen_e = false;
4466    let mut digits_before_dot = false;
4467    for (i, &c) in bytes.iter().enumerate() {
4468        match c {
4469            b'0'..=b'9' => {
4470                if !seen_dot && !seen_e {
4471                    digits_before_dot = true;
4472                }
4473                end = i + 1;
4474            }
4475            // A sign is only meaningful leading, or straight after the exponent
4476            // marker; it never completes a literal on its own.
4477            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
4478            // `1.` is a complete literal; a bare `.` is not.
4479            b'.' if !seen_dot && !seen_e => {
4480                seen_dot = true;
4481                if digits_before_dot {
4482                    end = i + 1;
4483                }
4484            }
4485            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
4486            _ => break,
4487        }
4488    }
4489    if end == 0 {
4490        return f64::NAN;
4491    }
4492    t[..end].parse::<f64>().unwrap_or(f64::NAN)
4493}
4494
4495/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
4496/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
4497/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
4498/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
4499/// rather than 1.
4500pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
4501    if exp == 0.0 {
4502        return 1.0;
4503    }
4504    if base.is_nan() || exp.is_nan() {
4505        return f64::NAN;
4506    }
4507    if base.abs() == 1.0 && exp.is_infinite() {
4508        return f64::NAN;
4509    }
4510    base.powf(exp)
4511}
4512
4513fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
4514    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
4515    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
4516    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
4517    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
4518    // exception: it never reads an argument, so `Math.random(1n)` is fine.
4519    if fname != "random"
4520        && args
4521            .iter()
4522            .any(|a| with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))))
4523    {
4524        return Err(host::type_error(
4525            "Cannot convert a BigInt value to a number",
4526        ));
4527    }
4528    let x = arg_num(args, 0);
4529    let r = match fname {
4530        "floor" => x.floor(),
4531        "ceil" => x.ceil(),
4532        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
4533        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
4534        // before the floor sees it, so it answers 1 for the largest double below
4535        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
4536        // perturbs integers above 2^52, where `x + 0.5` is no longer
4537        // representable (`Math.round(4503599627370497)` must be the input).
4538        // Splitting the zero-band cases out first also carries the signed zero
4539        // the spec asks for without a post-hoc patch.
4540        "round" => {
4541            if !x.is_finite() || x == 0.0 {
4542                x
4543            } else if x > 0.0 && x < 0.5 {
4544                0.0
4545            } else if (-0.5..0.0).contains(&x) {
4546                -0.0
4547            } else {
4548                // |x| >= 0.5, so `floor` and the subtraction are both exact
4549                // (every double >= 2^52 is already an integer and yields 0 here).
4550                let f = x.floor();
4551                if x - f >= 0.5 {
4552                    f + 1.0
4553                } else {
4554                    f
4555                }
4556            }
4557        }
4558        "trunc" => x.trunc(),
4559        "abs" => x.abs(),
4560        "sign" => {
4561            if x.is_nan() {
4562                f64::NAN
4563            } else if x > 0.0 {
4564                1.0
4565            } else if x < 0.0 {
4566                -1.0
4567            } else {
4568                x
4569            }
4570        }
4571        "sqrt" => x.sqrt(),
4572        "cbrt" => x.cbrt(),
4573        "exp" => x.exp(),
4574        "log" => x.ln(),
4575        "log2" => x.log2(),
4576        "log10" => x.log10(),
4577        "sin" => x.sin(),
4578        "cos" => x.cos(),
4579        "tan" => x.tan(),
4580        "asin" => x.asin(),
4581        "acos" => x.acos(),
4582        "atan" => x.atan(),
4583        "atan2" => x.atan2(arg_num(args, 1)),
4584        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
4585        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
4586        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
4587        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
4588        // clause is shared.
4589        "pow" => js_pow(x, arg_num(args, 1)),
4590        // Hyperbolics and the two precision-preserving log/exp forms.
4591        "sinh" => x.sinh(),
4592        "cosh" => x.cosh(),
4593        "tanh" => x.tanh(),
4594        "asinh" => x.asinh(),
4595        "acosh" => x.acosh(),
4596        "atanh" => x.atanh(),
4597        "log1p" => x.ln_1p(),
4598        "expm1" => x.exp_m1(),
4599        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
4600        // wraparound, reinterpret as a signed 32-bit result.
4601        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
4602        "hypot" => {
4603            // Scale by the largest magnitude before squaring — this avoids the
4604            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
4605            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
4606            let mut max = 0.0f64;
4607            for x in &xs {
4608                if x.abs() > max {
4609                    max = x.abs();
4610                }
4611            }
4612            if xs.iter().any(|x| x.is_infinite()) {
4613                f64::INFINITY
4614            } else if max == 0.0 || !max.is_finite() {
4615                max
4616            } else {
4617                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
4618                max * s.sqrt()
4619            }
4620        }
4621        "random" => pseudo_random(),
4622        "max" => {
4623            if args.is_empty() {
4624                f64::NEG_INFINITY
4625            } else {
4626                let mut m = f64::NEG_INFINITY;
4627                for a in args {
4628                    let n = with_host(|h| h.to_number(a));
4629                    if n.is_nan() {
4630                        return Ok(Value::Float(f64::NAN));
4631                    }
4632                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
4633                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
4634                    // must not keep the -0 the first iteration installed.
4635                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
4636                        m = n;
4637                    }
4638                }
4639                m
4640            }
4641        }
4642        "min" => {
4643            if args.is_empty() {
4644                f64::INFINITY
4645            } else {
4646                let mut m = f64::INFINITY;
4647                for a in args {
4648                    let n = with_host(|h| h.to_number(a));
4649                    if n.is_nan() {
4650                        return Ok(Value::Float(f64::NAN));
4651                    }
4652                    // Mirror of `max`: -0 ranks below +0 even though `<` says
4653                    // they are equal, so `Math.min(0, -0)` is -0.
4654                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
4655                        m = n;
4656                    }
4657                }
4658                m
4659            }
4660        }
4661        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
4662        "clz32" => {
4663            let u = if x.is_finite() {
4664                x.trunc().rem_euclid(4294967296.0) as u32
4665            } else {
4666                0
4667            };
4668            u.leading_zeros() as f64
4669        }
4670        // Round to the nearest single-precision float.
4671        "fround" => (x as f32) as f64,
4672        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
4673    };
4674    Ok(Value::Float(r))
4675}
4676
4677/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
4678/// Node by nature; kept simple).
4679fn pseudo_random() -> f64 {
4680    use std::cell::Cell;
4681    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
4682    SEED.with(|s| {
4683        let mut x = s.get();
4684        x ^= x << 13;
4685        x ^= x >> 7;
4686        x ^= x << 17;
4687        s.set(x);
4688        (x >> 11) as f64 / (1u64 << 53) as f64
4689    })
4690}
4691
4692// ── Object.* ──────────────────────────────────────────────────────────────────
4693
4694fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
4695    let v = arg0(&args);
4696    require_object_coercible(&v)?;
4697    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
4698    // reports every own STRING key the trap named; the enumerating modes
4699    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
4700    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
4701        if mode == 3 {
4702            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
4703            return Ok(with_host(|h| {
4704                let out: Vec<Value> = keys
4705                    .into_iter()
4706                    .filter(|k| !host::is_symbol_key(k))
4707                    .map(|k| h.new_str(k))
4708                    .collect();
4709                h.new_array(out)
4710            }));
4711        }
4712        let entries = crate::proxy::own_enum_entries(&v)?;
4713        return Ok(with_host(|h| {
4714            let out: Vec<Value> = entries
4715                .into_iter()
4716                .map(|(k, val)| match mode {
4717                    0 => h.new_str(k),
4718                    1 => val,
4719                    _ => {
4720                        let ks = h.new_str(k);
4721                        h.new_array(vec![ks, val])
4722                    }
4723                })
4724                .collect();
4725            h.new_array(out)
4726        }));
4727    }
4728    // A builtin prototype namespace that exposes enumerable methods for copying
4729    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
4730    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&v).cloned()) {
4731        if let Some(names) = builtin_proto_method_names(&ns) {
4732            return Ok(with_host(|h| {
4733                let out: Vec<Value> = names
4734                    .iter()
4735                    .map(|name| match mode {
4736                        1 => h.alloc(JsObj::Builtin(format!(
4737                            "@proto:{}:{name}",
4738                            ns.trim_end_matches(".prototype")
4739                        ))),
4740                        2 => {
4741                            let ks = h.new_str(*name);
4742                            let val = h.alloc(JsObj::Builtin(format!(
4743                                "@proto:{}:{name}",
4744                                ns.trim_end_matches(".prototype")
4745                            )));
4746                            h.new_array(vec![ks, val])
4747                        }
4748                        _ => h.new_str(*name),
4749                    })
4750                    .collect();
4751                h.new_array(out)
4752            }));
4753        }
4754        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
4755        // keys are the members node-js implements, each resolved to the same
4756        // first-class value a property read would give.
4757        let mut names = crate::stdlib::namespace_keys(&ns);
4758        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
4759        // its members live in the builtin dispatch table. They are
4760        // non-enumerable in V8, so they surface only under
4761        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
4762        if names.is_empty() && mode == 3 {
4763            let prefix = format!("{ns}.");
4764            names = NS_METHODS
4765                .iter()
4766                .filter_map(|q| q.strip_prefix(&prefix))
4767                .map(|m| m.to_string())
4768                .collect();
4769        }
4770        if !names.is_empty() {
4771            let entries: Vec<(String, Value)> = names
4772                .into_iter()
4773                .map(|k| {
4774                    let val = namespace_property(&ns, &k);
4775                    (k, val)
4776                })
4777                .collect();
4778            return Ok(with_host(|h| {
4779                let out: Vec<Value> = entries
4780                    .into_iter()
4781                    .map(|(k, val)| match mode {
4782                        1 => val,
4783                        2 => {
4784                            let ks = h.new_str(k);
4785                            h.new_array(vec![ks, val])
4786                        }
4787                        _ => h.new_str(k),
4788                    })
4789                    .collect();
4790                h.new_array(out)
4791            }));
4792        }
4793    }
4794    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
4795    // non-enumerable ones, plus the exotic `length` an array carries.
4796    let entries: Vec<(String, Value)> = with_host(|h| {
4797        if mode == 3 {
4798            // An array's exotic `length` is already placed (after the indices,
4799            // before the ordinary string keys) by `own_key_names`.
4800            return h
4801                .own_key_names(&v, false)
4802                .into_iter()
4803                .map(|k| (k, Value::Undef))
4804                .collect();
4805        }
4806        Vec::new()
4807    });
4808    let entries = if mode == 3 {
4809        entries
4810    } else {
4811        host::own_enum_entries_deep(&v)
4812    };
4813    Ok(with_host(|h| {
4814        let out: Vec<Value> = entries
4815            .into_iter()
4816            .map(|(k, val)| match mode {
4817                0 | 3 => h.new_str(k),
4818                1 => val,
4819                _ => {
4820                    let ks = h.new_str(k);
4821                    h.new_array(vec![ks, val])
4822                }
4823            })
4824            .collect();
4825        h.new_array(out)
4826    }))
4827}
4828
4829fn object_assign(args: Vec<Value>) -> Result<Value, String> {
4830    let target = arg0(&args);
4831    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
4832    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
4833    require_object_coercible(&target)?;
4834    for src in args.iter().skip(1) {
4835        // `Object.assign` copies own *enumerable* properties, running any getter
4836        // — symbol-keyed ones included (7.3.25).
4837        let entries = host::own_enum_entries_deep(src);
4838        let syms = with_host(|h| h.own_symbol_entries(src));
4839        // A plain object target is filled in place (one borrow, then a single
4840        // re-canonicalization of the integer-index keys).
4841        let filled = with_host(|h| {
4842            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
4843                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
4844                    p.insert(k, v);
4845                }
4846                host::canonicalize_own_keys(p);
4847                return true;
4848            }
4849            false
4850        });
4851        // Any OTHER target — an array being the common one — goes through the
4852        // ordinary Set path. The in-place branch above matched `JsObj::Object`
4853        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
4854        // returned the untouched array: no error, just a missing property. The
4855        // Set path is what an `arr.extra = 9` assignment already used, so index
4856        // and non-index keys land where they do for a direct write.
4857        if !filled {
4858            for (k, v) in entries.into_iter().chain(syms) {
4859                set_property(&target, &k, v)?;
4860            }
4861        }
4862    }
4863    Ok(target)
4864}
4865
4866fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
4867    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
4868    let mut props: IndexMap<String, Value> = IndexMap::new();
4869    for p in pairs {
4870        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
4871        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
4872        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
4873        props.insert(key, val);
4874    }
4875    Ok(with_host(|h| h.new_object(props)))
4876}
4877
4878/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
4879/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
4880/// members mapped to that key, in first-seen key order.
4881fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
4882    let items = host::iter_all(&arg0(&args))?;
4883    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
4884    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
4885    for (i, item) in items.into_iter().enumerate() {
4886        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
4887        let key = with_host(|h| h.property_key(&key_v));
4888        groups.entry(key).or_default().push(item);
4889    }
4890    let props: IndexMap<String, Value> = with_host(|h| {
4891        groups
4892            .into_iter()
4893            .map(|(k, v)| (k, h.new_array(v)))
4894            .collect()
4895    });
4896    let obj = with_host(|h| h.new_object(props));
4897    // A null-prototype object (as Node returns), so it has no inherited members.
4898    with_host(|h| {
4899        let nv = h.null();
4900        h.set_proto(&obj, nv);
4901    });
4902    Ok(obj)
4903}
4904
4905/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
4906/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
4907fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
4908    let items = host::iter_all(&arg0(&args))?;
4909    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
4910    let m = with_host(|h| {
4911        h.alloc(JsObj::Map {
4912            entries: IndexMap::new(),
4913            weak: false,
4914        })
4915    });
4916    for (i, item) in items.into_iter().enumerate() {
4917        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
4918        let existing = map_method(&m, "get", vec![key_v.clone()])?;
4919        if matches!(existing, Value::Undef) {
4920            let arr = with_host(|h| h.new_array(vec![item]));
4921            map_method(&m, "set", vec![key_v, arr])?;
4922        } else {
4923            with_host(|h| {
4924                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
4925                    a.push(item);
4926                }
4927            });
4928        }
4929    }
4930    Ok(m)
4931}
4932
4933/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
4934/// element and each `mapFn` result.
4935///
4936/// Written in JavaScript and compiled once, because the operation IS an async
4937/// function: a Rust builtin runs outside any coroutine and has no way to await,
4938/// so draining a promise from there would mean running the microtask queue by
4939/// hand. Delegating to the engine's own `async`/`for await` keeps the
4940/// suspension semantics — and the ordering they imply — exactly the language's.
4941///
4942/// The source may be an async iterable, a sync iterable, a bare iterator, or an
4943/// array-like. Everything iterable goes through `for await`, which awaits a sync
4944/// source's elements individually — that is what makes
4945/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
4946/// accepted because an async generator object does not expose
4947/// `Symbol.asyncIterator` on this frontend.
4948fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
4949    thread_local! {
4950        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
4951    }
4952    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
4953        const out = []; let i = 0;\n\
4954        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
4955        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
4956            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
4957        if (iterable) {\n\
4958            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
4959            return out;\n\
4960        }\n\
4961        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
4962        while (i < len) { await step(items[i]); }\n\
4963        return out;\n\
4964    })";
4965    let f = IMPL.with(|c| c.borrow().clone());
4966    let f = match f {
4967        Some(f) => f,
4968        None => {
4969            let f = crate::eval_in_global_scope(SRC)?;
4970            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
4971            f
4972        }
4973    };
4974    host::invoke(&f, args, None)
4975}
4976
4977fn array_from(args: Vec<Value>) -> Result<Value, String> {
4978    // `Array.from` accepts generators and user iterables, plus array-likes with a
4979    // numeric `.length`.
4980    let src = arg0(&args);
4981    let items = match host::iter_all(&src) {
4982        Ok(v) => v,
4983        Err(_) => array_like_items(&src),
4984    };
4985    if let Some(cb) = args.get(1).cloned() {
4986        let mut out = Vec::with_capacity(items.len());
4987        for (i, it) in items.into_iter().enumerate() {
4988            out.push(host::invoke(&cb, vec![it, Value::Float(i as f64)], None)?);
4989        }
4990        return Ok(with_host(|h| h.new_array(out)));
4991    }
4992    Ok(with_host(|h| h.new_array(items)))
4993}
4994
4995/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
4996fn array_like_items(src: &Value) -> Vec<Value> {
4997    let len = get_property(src, "length")
4998        .ok()
4999        .map(|l| with_host(|h| h.to_number(&l)))
5000        .unwrap_or(0.0);
5001    if !len.is_finite() || len <= 0.0 {
5002        return Vec::new();
5003    }
5004    (0..len as usize)
5005        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
5006        .collect()
5007}
5008
5009// ── JSON ──────────────────────────────────────────────────────────────────────
5010
5011fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
5012    // A CALLABLE second argument is the replacer function, and it is checked
5013    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
5014    // callable never also reaches the key-filter path below.
5015    let replacer = args
5016        .get(1)
5017        .filter(|r| with_host(|h| host::is_callable(h, r)))
5018        .cloned();
5019    // `toJSON` and the replacer run BEFORE serialization and are user code, so
5020    // the tree is rewritten first — outside the host borrow `json_str` holds,
5021    // and before the BigInt walk, which has no cycle guard of its own.
5022    //
5023    // The top-level value is a property of a synthetic wrapper `{ "": value }`
5024    // under key `""`, which is exactly the holder the replacer receives as
5025    // `this` on its first call.
5026    let root = arg0(&args);
5027    let wrapper = with_host(|h| {
5028        let mut m: IndexMap<String, Value> = IndexMap::new();
5029        m.insert(String::new(), root.clone());
5030        h.new_object(m)
5031    });
5032    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
5033    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
5034    // bigint form), matching Node's exact message.
5035    if with_host(|h| json_has_bigint(h, &v)) {
5036        return Err(host::type_error("Do not know how to serialize a BigInt"));
5037    }
5038    let indent = match args.get(2) {
5039        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
5040        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
5041        None => String::new(),
5042    };
5043    // A replacer array (args[1]) restricts which object keys are serialized.
5044    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
5045        with_host(|h| match h.get(r) {
5046            Some(JsObj::Array(items)) => {
5047                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
5048            }
5049            _ => None,
5050        })
5051    });
5052    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
5053    match s {
5054        Some(s) => Ok(with_host(|h| h.new_str(s))),
5055        None => Ok(Value::Undef),
5056    }
5057}
5058
5059/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
5060/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
5061/// function as `replacer.call(holder, key, value)`, then recurse into whatever
5062/// object survives. Applies to user methods, class methods, and the native
5063/// `Date`/`Buffer`/`URL` accessors alike.
5064///
5065/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
5066/// objects currently being walked so a cyclic structure is reported rather than
5067/// spinning forever.
5068///
5069/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
5070/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
5071/// because the inner method is a plain (unserializable) function property of the
5072/// returned object, not a second conversion hook.
5073fn apply_to_json(
5074    holder: &Value,
5075    key: &str,
5076    v: &Value,
5077    path: &mut Vec<Value>,
5078    rep: Option<&Value>,
5079) -> Result<Value, String> {
5080    let mut v = v.clone();
5081    if matches!(v, Value::Obj(_)) {
5082        let tag = crate::stdlib::native_tag(&v);
5083        let has_to_json = with_host(|h| match host::lookup_chain(h, &v, "toJSON") {
5084            Some(f) => host::is_callable(h, &f),
5085            None => false,
5086        }) || tag
5087            .as_deref()
5088            .map(crate::stdlib::has_to_json)
5089            .unwrap_or(false);
5090        if has_to_json {
5091            let k = with_host(|h| h.new_str(key.to_string()));
5092            v = host::call_method(&v, "toJSON", vec![k])?;
5093        }
5094    }
5095    if let Some(rep) = rep {
5096        let k = with_host(|h| h.new_str(key.to_string()));
5097        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
5098    }
5099    json_walk_children(&v, path, rep)
5100}
5101
5102/// Whether a raw property key of a host object is one `json_str` serializes. The
5103/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
5104/// invisible to JSON, so the replacer must not be invoked for them either.
5105fn json_visible_key(k: &str) -> bool {
5106    !k.starts_with("@@") && !k.starts_with('#')
5107}
5108
5109/// Recurse into the elements/properties of an already-converted value, running
5110/// `apply_to_json` for each with this value as the holder.
5111fn json_walk_children(
5112    v: &Value,
5113    path: &mut Vec<Value>,
5114    rep: Option<&Value>,
5115) -> Result<Value, String> {
5116    if !matches!(v, Value::Obj(_)) {
5117        return Ok(v.clone());
5118    }
5119    // A value that contains itself has no JSON form.
5120    if with_host(|h| path.iter().any(|p| h.strict_eq(p, v))) {
5121        return Err(host::type_error("Converting circular structure to JSON"));
5122    }
5123    // A Proxy owns no property map, so it is snapshotted through its traps into
5124    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
5125    // — which read every member through `[[Get]]`, exactly as the snapshot does.
5126    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
5127        let snap = crate::proxy::json_snapshot(v)?;
5128        path.push(v.clone());
5129        let out = json_walk_children(&snap, path, rep);
5130        path.pop();
5131        return out;
5132    }
5133    let obj = with_host(|h| h.get(v).cloned());
5134    path.push(v.clone());
5135    let out = (|| match obj {
5136        Some(JsObj::Array(items)) => {
5137            let mut out = Vec::with_capacity(items.len());
5138            let mut changed = false;
5139            for (i, it) in items.iter().enumerate() {
5140                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
5141                changed |= !with_host(|h| h.strict_eq(&nv, it));
5142                out.push(nv);
5143            }
5144            // Keep identity when nothing changed, so an enclosing object is not
5145            // needlessly rebuilt (which would drop its property attributes).
5146            if changed {
5147                Ok(with_host(|h| h.new_array(out)))
5148            } else {
5149                Ok(v.clone())
5150            }
5151        }
5152        Some(JsObj::Object(props)) => {
5153            // An enumerable own accessor must have its getter RUN and the result
5154            // serialized. That cannot happen inside `json_str` (which holds the
5155            // host borrow), so materialize here — the same reason `toJSON` is
5156            // applied in this pass.
5157            let has_accessor = with_host(|h| {
5158                h.own_accessor_keys(v)
5159                    .iter()
5160                    .any(|k| h.prop_attrs(v, k).enumerable)
5161            });
5162            if has_accessor {
5163                let mut next: IndexMap<String, Value> = IndexMap::new();
5164                for (k, val) in host::own_enum_entries_deep(v) {
5165                    let nv = if json_visible_key(&k) {
5166                        apply_to_json(v, &k, &val, path, rep)?
5167                    } else {
5168                        val
5169                    };
5170                    next.insert(k, nv);
5171                }
5172                return Ok(with_host(|h| h.new_object(next)));
5173            }
5174            // Only rebuild when a descendant actually changed, so plain data keeps
5175            // its identity (and its prototype / native tag).
5176            let mut next: IndexMap<String, Value> = IndexMap::new();
5177            let mut changed = false;
5178            for (k, val) in &props {
5179                let nv = if json_visible_key(k) {
5180                    apply_to_json(v, k, val, path, rep)?
5181                } else {
5182                    val.clone()
5183                };
5184                changed |= !with_host(|h| h.strict_eq(&nv, val));
5185                next.insert(k.clone(), nv);
5186            }
5187            if changed {
5188                Ok(with_host(|h| {
5189                    let o = h.new_object(next);
5190                    h.copy_prop_attrs(v, &o);
5191                    o
5192                }))
5193            } else {
5194                Ok(v.clone())
5195            }
5196        }
5197        _ => Ok(v.clone()),
5198    })();
5199    path.pop();
5200    out
5201}
5202
5203/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
5204/// try to serialize (a value in an array/object) — such a value throws.
5205fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
5206    match h.get(v) {
5207        Some(JsObj::BigInt(_)) => true,
5208        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
5209        Some(JsObj::Object(props)) => props
5210            .iter()
5211            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
5212            .any(|(_, val)| json_has_bigint(h, val)),
5213        _ => false,
5214    }
5215}
5216
5217fn json_str(
5218    h: &host::JsHost,
5219    v: &Value,
5220    indent: &str,
5221    depth: usize,
5222    keys: Option<&[String]>,
5223) -> Option<String> {
5224    let sep = if indent.is_empty() { ":" } else { ": " };
5225    match v {
5226        Value::Undef => None,
5227        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
5228        Value::Int(n) => Some(n.to_string()),
5229        Value::Float(f) => Some(if f.is_finite() {
5230            host::fmt_number(*f)
5231        } else {
5232            "null".into()
5233        }),
5234        Value::Str(s) => Some(json_quote(s)),
5235        Value::Obj(_) => match h.get(v) {
5236            Some(JsObj::Str(s)) => Some(json_quote(s)),
5237            Some(JsObj::Null) => Some("null".into()),
5238            // Map/Set have no enumerable own string keys → serialize as `{}`.
5239            Some(JsObj::Map { .. }) | Some(JsObj::Set { .. }) => Some("{}".into()),
5240            // Functions and symbols are omitted (undefined) as values.
5241            Some(JsObj::Func(_))
5242            | Some(JsObj::Builtin(_))
5243            | Some(JsObj::BoundMethod { .. })
5244            | Some(JsObj::BoundFunc { .. })
5245            | Some(JsObj::Class(_))
5246            | Some(JsObj::Symbol { .. })
5247            | Some(JsObj::Generator { .. }) => None,
5248            Some(JsObj::Array(items)) => {
5249                if items.is_empty() {
5250                    return Some("[]".into());
5251                }
5252                let parts: Vec<String> = items
5253                    .iter()
5254                    .map(|x| {
5255                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
5256                    })
5257                    .collect();
5258                Some(wrap(&parts, "[", "]", indent, depth))
5259            }
5260            Some(JsObj::Object(props)) => {
5261                // A replacer array restricts (and orders) which keys are emitted.
5262                let parts: Vec<String> = match keys {
5263                    Some(allow) => allow
5264                        .iter()
5265                        .filter_map(|k| {
5266                            props.get(k).and_then(|val| {
5267                                json_str(h, val, indent, depth + 1, keys)
5268                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
5269                            })
5270                        })
5271                        .collect(),
5272                    None => h
5273                        .own_enum_entries(v)
5274                        .iter()
5275                        .filter_map(|(k, val)| {
5276                            json_str(h, val, indent, depth + 1, keys)
5277                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
5278                        })
5279                        .collect(),
5280                };
5281                if parts.is_empty() {
5282                    return Some("{}".into());
5283                }
5284                Some(wrap(&parts, "{", "}", indent, depth))
5285            }
5286            _ => Some("null".into()),
5287        },
5288        _ => Some("null".into()),
5289    }
5290}
5291
5292fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
5293    if indent.is_empty() {
5294        format!("{open}{}{close}", parts.join(","))
5295    } else {
5296        let pad = indent.repeat(depth + 1);
5297        let pad_close = indent.repeat(depth);
5298        format!(
5299            "{open}\n{pad}{}\n{pad_close}{close}",
5300            parts.join(&format!(",\n{pad}"))
5301        )
5302    }
5303}
5304
5305fn json_quote(s: &str) -> String {
5306    let mut out = String::from("\"");
5307    for c in s.chars() {
5308        match c {
5309            '"' => out.push_str("\\\""),
5310            '\\' => out.push_str("\\\\"),
5311            '\n' => out.push_str("\\n"),
5312            '\t' => out.push_str("\\t"),
5313            '\r' => out.push_str("\\r"),
5314            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
5315            // Backspace and form feed were missing, so they fell through to the
5316            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
5317            // `""` where node produces `"\b"`. Both parse back to the same
5318            // string, so the difference is invisible to a round trip and shows
5319            // up only as a byte mismatch against a fixture or a checksum.
5320            '\u{8}' => out.push_str("\\b"),
5321            '\u{c}' => out.push_str("\\f"),
5322            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
5323            _ => out.push(c),
5324        }
5325    }
5326    out.push('"');
5327    out
5328}
5329
5330fn json_parse(args: Vec<Value>) -> Result<Value, String> {
5331    let s = with_host(|h| h.str_of(&arg0(&args)));
5332    let mut p = JsonParser {
5333        chars: s.chars().collect(),
5334        pos: 0,
5335    };
5336    p.skip_ws();
5337    if p.peek().is_none() {
5338        return Err("SyntaxError: Unexpected end of JSON input".into());
5339    }
5340    let v = p.parse_value()?;
5341    let value_end = p.pos;
5342    p.skip_ws();
5343    // Anything after the top-level value is an error — the parser used to accept
5344    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
5345    if let Some(c) = p.peek() {
5346        // V8 names the token kind only when it butts directly against the value
5347        // (`01` -> "Unexpected number at position 1"); with whitespace between
5348        // it is just a non-whitespace character (`1 2`).
5349        // Only a digit butted directly against a completed number literal —
5350        // V8's number scanner is still in number context there. `5"x"` and
5351        // `[0,1]0` exit the scanner cleanly and get the generic message.
5352        let after_number = value_end > 0
5353            && p.pos == value_end
5354            && p.chars[value_end - 1].is_ascii_digit()
5355            && c.is_ascii_digit();
5356        return Err(if after_number {
5357            p.err_at("Unexpected number", p.pos)
5358        } else {
5359            p.err_trailing(p.pos)
5360        });
5361    }
5362    // Optional reviver: walk bottom-up, transforming each (key, value).
5363    if let Some(reviver) = args
5364        .get(1)
5365        .filter(|r| with_host(|h| host::is_callable(h, r)))
5366        .cloned()
5367    {
5368        return json_revive("", v, &reviver);
5369    }
5370    Ok(v)
5371}
5372
5373/// `JSON.parse` reviver walk: recurse into children first, then call
5374/// `reviver(key, value)`; a returned `undefined` drops the property.
5375fn json_revive(key: &str, val: Value, reviver: &Value) -> Result<Value, String> {
5376    match with_host(|h| h.get(&val).cloned()) {
5377        Some(JsObj::Array(items)) => {
5378            for i in 0..items.len() {
5379                let elem = with_host(|h| match h.get(&val) {
5380                    Some(JsObj::Array(it)) => it[i].clone(),
5381                    _ => Value::Undef,
5382                });
5383                let nv = json_revive(&i.to_string(), elem, reviver)?;
5384                with_host(|h| {
5385                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
5386                        it[i] = nv;
5387                    }
5388                });
5389            }
5390        }
5391        Some(JsObj::Object(props)) => {
5392            let keys: Vec<String> = props
5393                .keys()
5394                .filter(|k| !k.starts_with("@@"))
5395                .cloned()
5396                .collect();
5397            for k in keys {
5398                let elem = with_host(|h| match h.get(&val) {
5399                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
5400                    _ => Value::Undef,
5401                });
5402                let nv = json_revive(&k, elem, reviver)?;
5403                with_host(|h| {
5404                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
5405                        if matches!(nv, Value::Undef) {
5406                            p.shift_remove(&k);
5407                        } else {
5408                            p.insert(k.clone(), nv);
5409                        }
5410                    }
5411                });
5412            }
5413        }
5414        _ => {}
5415    }
5416    let kv = with_host(|h| h.new_str(key.to_string()));
5417    host::invoke(reviver, vec![kv, val], None)
5418}
5419
5420struct JsonParser {
5421    chars: Vec<char>,
5422    pos: usize,
5423}
5424impl JsonParser {
5425    fn peek(&self) -> Option<char> {
5426        self.chars.get(self.pos).copied()
5427    }
5428
5429    /// `at position N (line L column C)` — the location suffix V8 appends to the
5430    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
5431    /// node-js counts `char`s, which agree for the BMP.
5432    fn at(&self, pos: usize) -> String {
5433        let mut line = 1usize;
5434        let mut col = 1usize;
5435        for c in &self.chars[..pos.min(self.chars.len())] {
5436            if *c == '\n' {
5437                line += 1;
5438                col = 1;
5439            } else {
5440                col += 1;
5441            }
5442        }
5443        format!(" at position {pos} (line {line} column {col})")
5444    }
5445
5446    /// A positional error (`Expected ':' after property name in JSON at …`).
5447    fn err_at(&self, what: &str, pos: usize) -> String {
5448        format!("SyntaxError: {what} in JSON{}", self.at(pos))
5449    }
5450
5451    /// The one positional message V8 does NOT suffix with `in JSON`.
5452    fn err_trailing(&self, pos: usize) -> String {
5453        format!(
5454            "SyntaxError: Unexpected non-whitespace character after JSON{}",
5455            self.at(pos)
5456        )
5457    }
5458
5459    /// V8's default parse error: the offending character plus a window of the
5460    /// source. The whole input is quoted when it is short (<= 20 chars);
5461    /// otherwise a 10-character context window either side of `pos` is shown,
5462    /// elided with `...` on whichever side was cut.
5463    fn err_token(&self, pos: usize) -> String {
5464        const MAX_WHOLE: usize = 20;
5465        const CONTEXT: usize = 10;
5466        let len = self.chars.len();
5467        let Some(c) = self.chars.get(pos) else {
5468            return "SyntaxError: Unexpected end of JSON input".into();
5469        };
5470        // V8 reports the whole input for the JS literals that are famously not
5471        // JSON, without naming an offending character.
5472        let whole: String = self.chars.iter().collect();
5473        if matches!(
5474            whole.as_str(),
5475            "undefined" | "NaN" | "Infinity" | "-Infinity"
5476        ) {
5477            return format!("SyntaxError: \"{whole}\" is not valid JSON");
5478        }
5479        let snippet = if len <= MAX_WHOLE {
5480            format!("\"{whole}\"")
5481        } else {
5482            let start = pos.saturating_sub(CONTEXT);
5483            let end = (pos + CONTEXT).min(len);
5484            let body: String = self.chars[start..end].iter().collect();
5485            let head = if start > 0 { "..." } else { "" };
5486            let tail = if end < len { "..." } else { "" };
5487            format!("{head}\"{body}\"{tail}")
5488        };
5489        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
5490    }
5491
5492    fn skip_ws(&mut self) {
5493        while matches!(
5494            self.peek(),
5495            Some(' ') | Some('\n') | Some('\t') | Some('\r')
5496        ) {
5497            self.pos += 1;
5498        }
5499    }
5500    fn parse_value(&mut self) -> Result<Value, String> {
5501        self.skip_ws();
5502        match self.peek() {
5503            Some('{') => self.parse_object(),
5504            Some('[') => self.parse_array(),
5505            Some('"') => {
5506                let s = self.parse_string()?;
5507                Ok(with_host(|h| h.new_str(s)))
5508            }
5509            Some('t') | Some('f') => self.parse_bool(),
5510            Some('n') => {
5511                self.expect_lit("null")?;
5512                Ok(with_host(|h| h.null()))
5513            }
5514            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
5515            None => Err("SyntaxError: Unexpected end of JSON input".into()),
5516            _ => Err(self.err_token(self.pos)),
5517        }
5518    }
5519    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
5520        for ch in lit.chars() {
5521            match self.peek() {
5522                Some(c) if c == ch => self.pos += 1,
5523                // V8 reports the first character that broke the literal, which is
5524                // why `foo` complains about `'o'` (index 2) and not `'f'`.
5525                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
5526                _ => return Err(self.err_token(self.pos)),
5527            }
5528        }
5529        Ok(())
5530    }
5531    fn parse_bool(&mut self) -> Result<Value, String> {
5532        if self.peek() == Some('t') {
5533            self.expect_lit("true")?;
5534            Ok(Value::Bool(true))
5535        } else {
5536            self.expect_lit("false")?;
5537            Ok(Value::Bool(false))
5538        }
5539    }
5540    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
5541    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
5542    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
5543    /// it. Each way the grammar can run out has its own message.
5544    fn parse_number(&mut self) -> Result<Value, String> {
5545        let start = self.pos;
5546        if self.peek() == Some('-') {
5547            self.pos += 1;
5548            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5549                return Err(self.err_at("No number after minus sign", self.pos));
5550            }
5551        }
5552        if self.peek() == Some('0') {
5553            self.pos += 1;
5554        } else {
5555            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5556                self.pos += 1;
5557            }
5558        }
5559        if self.peek() == Some('.') {
5560            self.pos += 1;
5561            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5562                return Err(self.err_at("Unterminated fractional number", self.pos));
5563            }
5564            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5565                self.pos += 1;
5566            }
5567        }
5568        if matches!(self.peek(), Some('e') | Some('E')) {
5569            self.pos += 1;
5570            if matches!(self.peek(), Some('+') | Some('-')) {
5571                self.pos += 1;
5572            }
5573            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5574                return Err(self.err_at("Exponent part is missing a number", self.pos));
5575            }
5576            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5577                self.pos += 1;
5578            }
5579        }
5580        let s: String = self.chars[start..self.pos].iter().collect();
5581        s.parse::<f64>()
5582            .map(Value::Float)
5583            .map_err(|_| self.err_at("Unexpected number", start))
5584    }
5585    fn parse_string(&mut self) -> Result<String, String> {
5586        self.pos += 1; // opening quote
5587        let mut out = String::new();
5588        loop {
5589            match self.peek() {
5590                None => return Err(self.err_at("Unterminated string", self.pos)),
5591                Some('"') => {
5592                    self.pos += 1;
5593                    break;
5594                }
5595                Some('\\') => {
5596                    self.pos += 1;
5597                    match self.peek() {
5598                        Some('n') => out.push('\n'),
5599                        Some('t') => out.push('\t'),
5600                        Some('r') => out.push('\r'),
5601                        Some('"') => out.push('"'),
5602                        Some('\\') => out.push('\\'),
5603                        Some('/') => out.push('/'),
5604                        Some('b') => out.push('\u{08}'),
5605                        Some('f') => out.push('\u{0C}'),
5606                        Some('u') => {
5607                            let h: String = self.chars
5608                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
5609                                .iter()
5610                                .collect();
5611                            if let Ok(n) = u32::from_str_radix(&h, 16) {
5612                                if let Some(ch) = char::from_u32(n) {
5613                                    out.push(ch);
5614                                }
5615                            }
5616                            self.pos += 4;
5617                        }
5618                        _ => {}
5619                    }
5620                    self.pos += 1;
5621                }
5622                // A raw control character is not legal inside a JSON string; it
5623                // has to be escaped. V8 rejects it rather than passing it through.
5624                Some(c) if (c as u32) < 0x20 => {
5625                    return Err(self.err_at("Bad control character in string literal", self.pos))
5626                }
5627                Some(c) => {
5628                    out.push(c);
5629                    self.pos += 1;
5630                }
5631            }
5632        }
5633        Ok(out)
5634    }
5635    fn parse_array(&mut self) -> Result<Value, String> {
5636        self.pos += 1; // [
5637        let mut items = Vec::new();
5638        self.skip_ws();
5639        if self.peek() == Some(']') {
5640            self.pos += 1;
5641            return Ok(with_host(|h| h.new_array(items)));
5642        }
5643        loop {
5644            items.push(self.parse_value()?);
5645            self.skip_ws();
5646            match self.peek() {
5647                Some(',') => {
5648                    self.pos += 1;
5649                }
5650                Some(']') => {
5651                    self.pos += 1;
5652                    break;
5653                }
5654                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
5655            }
5656        }
5657        Ok(with_host(|h| h.new_array(items)))
5658    }
5659    fn parse_object(&mut self) -> Result<Value, String> {
5660        self.pos += 1; // {
5661        let mut props: IndexMap<String, Value> = IndexMap::new();
5662        self.skip_ws();
5663        if self.peek() == Some('}') {
5664            self.pos += 1;
5665            return Ok(with_host(|h| h.new_object(props)));
5666        }
5667        loop {
5668            self.skip_ws();
5669            if self.peek() != Some('"') {
5670                // The first key uses the "or '}'" wording (an empty object is
5671                // still legal there); a key after a comma does not. End of input
5672                // reports the same expectation, at the end position.
5673                return Err(if props.is_empty() {
5674                    self.err_at("Expected property name or '}'", self.pos)
5675                } else {
5676                    self.err_at("Expected double-quoted property name", self.pos)
5677                });
5678            }
5679            let key = self.parse_string()?;
5680            self.skip_ws();
5681            if self.peek() != Some(':') {
5682                return Err(match self.peek() {
5683                    None => "SyntaxError: Unexpected end of JSON input".into(),
5684                    _ => self.err_at("Expected ':' after property name", self.pos),
5685                });
5686            }
5687            self.pos += 1;
5688            let val = self.parse_value()?;
5689            props.insert(key, val);
5690            self.skip_ws();
5691            match self.peek() {
5692                Some(',') => {
5693                    self.pos += 1;
5694                }
5695                Some('}') => {
5696                    self.pos += 1;
5697                    break;
5698                }
5699                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
5700            }
5701        }
5702        Ok(with_host(|h| h.new_object(props)))
5703    }
5704}
5705
5706// ══ type methods (array / string / number) ═══════════════════════════════════
5707
5708fn is_array_method(name: &str) -> bool {
5709    matches!(
5710        name,
5711        "push"
5712            | "pop"
5713            | "shift"
5714            | "unshift"
5715            | "map"
5716            | "filter"
5717            | "forEach"
5718            | "join"
5719            | "slice"
5720            | "indexOf"
5721            | "lastIndexOf"
5722            | "includes"
5723            | "reduce"
5724            | "concat"
5725            | "reverse"
5726            | "sort"
5727            | "find"
5728            | "findIndex"
5729            | "some"
5730            | "every"
5731            | "flat"
5732            | "fill"
5733            | "splice"
5734            | "keys"
5735            | "values"
5736            | "entries"
5737            | "flatMap"
5738            | "at"
5739            | "toString"
5740            | "reduceRight"
5741            | "findLast"
5742            | "findLastIndex"
5743            | "copyWithin"
5744    )
5745}
5746fn is_string_method(name: &str) -> bool {
5747    matches!(
5748        name,
5749        "toUpperCase"
5750            | "toLowerCase"
5751            | "charAt"
5752            | "charCodeAt"
5753            | "codePointAt"
5754            | "indexOf"
5755            | "lastIndexOf"
5756            | "includes"
5757            | "slice"
5758            | "substring"
5759            | "substr"
5760            | "split"
5761            | "trim"
5762            | "trimStart"
5763            | "trimEnd"
5764            | "replace"
5765            | "replaceAll"
5766            | "repeat"
5767            | "startsWith"
5768            | "endsWith"
5769            | "padStart"
5770            | "padEnd"
5771            | "concat"
5772            | "at"
5773            | "toString"
5774            | "toLocaleString"
5775            | "valueOf"
5776            | "match"
5777            | "matchAll"
5778            | "search"
5779            | "normalize"
5780            | "localeCompare"
5781            | "toLocaleUpperCase"
5782            | "toLocaleLowerCase"
5783            | "isWellFormed"
5784            | "toWellFormed"
5785    )
5786}
5787
5788/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
5789fn is_regexp_arg(v: &Value) -> bool {
5790    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
5791}
5792
5793/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
5794/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
5795fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
5796    if pat.is_empty() {
5797        return Ok(s.to_string());
5798    }
5799    let mut out = String::new();
5800    let mut rest = s;
5801    let mut base = 0usize;
5802    while let Some(pos) = rest.find(pat) {
5803        out.push_str(&rest[..pos]);
5804        let offset = base + pos;
5805        let m = with_host(|h| h.new_str(pat.to_string()));
5806        let str_arg = with_host(|h| h.new_str(s.to_string()));
5807        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
5808        out.push_str(&with_host(|h| h.str_of(&r)));
5809        let consumed = pos + pat.len();
5810        base += consumed;
5811        rest = &rest[consumed..];
5812        if !all {
5813            break;
5814        }
5815    }
5816    out.push_str(rest);
5817    Ok(out)
5818}
5819fn is_number_method(name: &str) -> bool {
5820    matches!(
5821        name,
5822        "toFixed" | "toExponential" | "toString" | "toPrecision" | "toLocaleString" | "valueOf"
5823    )
5824}
5825
5826/// Dispatch `recv.name(args)` for the built-in prototype methods.
5827pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
5828    // `Object.prototype.valueOf` is inherited by every exotic that does not
5829    // override it (an Array does not), and returns the receiver. Without this
5830    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
5831    // and threw `valueOf is not a function`.
5832    if name == "valueOf"
5833        && matches!(
5834            with_host(|h| h.kind_of(recv)),
5835            Some(
5836                ObjKind::Array
5837                    | ObjKind::Map
5838                    | ObjKind::Set
5839                    | ObjKind::Generator
5840                    | ObjKind::Promise
5841                    | ObjKind::Iter
5842                    | ObjKind::RegExp
5843            )
5844        )
5845    {
5846        return Ok(recv.clone());
5847    }
5848    // Only the tag is needed to pick the branch — cloning the receiver here made
5849    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
5850    match with_host(|h| h.kind_of(recv)) {
5851        Some(ObjKind::Array) => array_method(recv, name, args),
5852        Some(ObjKind::Str) => {
5853            // `string_method` consumes the text itself, so this clone is the
5854            // payload, not a tag probe.
5855            let s = peek(recv, |o| match o {
5856                JsObj::Str(s) => Some(s.clone()),
5857                _ => None,
5858            })
5859            .unwrap_or_default();
5860            string_method(&s, name, args)
5861        }
5862        Some(ObjKind::Map) => map_method(recv, name, args),
5863        Some(ObjKind::Set) => set_method(recv, name, args),
5864        Some(ObjKind::Generator) => generator_method(recv, name, args),
5865        Some(ObjKind::Promise) => promise_method(recv, name, args),
5866        Some(ObjKind::Iter) => iter_method(recv, name, args),
5867        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
5868        Some(ObjKind::BigInt) => {
5869            let b = peek(recv, |o| match o {
5870                JsObj::BigInt(b) => Some(b.clone()),
5871                _ => None,
5872            })
5873            .unwrap_or_default();
5874            bigint_method(&b, name, args)
5875        }
5876        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
5877        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
5878            match function_builtin_method(recv, name, &args)? {
5879                Some(v) => Ok(v),
5880                None => Err(host::type_error(&format!("{name} is not a function"))),
5881            }
5882        }
5883        Some(ObjKind::Object) => {
5884            if let Some(f) = peek(recv, |o| match o {
5885                JsObj::Object(p) => p.get(name).cloned(),
5886                _ => None,
5887            }) {
5888                host::invoke(&f, args, Some(recv.clone()))
5889            } else if name == "hasOwnProperty" {
5890                let k = with_host(|h| h.str_of(&arg0(&args)));
5891                let has = peek(recv, |o| match o {
5892                    JsObj::Object(p) => Some(p.contains_key(&k)),
5893                    _ => None,
5894                })
5895                .unwrap_or(false);
5896                Ok(Value::Bool(has))
5897            } else if name == "toString" {
5898                Ok(with_host(|h| h.new_str("[object Object]")))
5899            } else {
5900                Err(host::type_error(&format!("{} is not a function", name)))
5901            }
5902        }
5903        _ => {
5904            // Primitive number/bool/string coercions.
5905            if let Value::Float(_) | Value::Int(_) = recv {
5906                return number_method(with_host(|h| h.to_number(recv)), name, args);
5907            }
5908            if let Some(s) = with_host(|h| h.as_str(recv)) {
5909                return string_method(&s, name, args);
5910            }
5911            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
5912            // so it reached no branch at all and `true.toString()` threw `is not
5913            // a function`. Its three methods are `toString`, `valueOf`, and the
5914            // inherited `Object.prototype.toLocaleString` — which
5915            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
5916            // was reachable from the array form too.
5917            if let Value::Bool(b) = recv {
5918                return match name {
5919                    "toString" | "toLocaleString" => {
5920                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
5921                    }
5922                    "valueOf" => Ok(Value::Bool(*b)),
5923                    _ => Err(host::type_error(&format!("{name} is not a function"))),
5924                };
5925            }
5926            Err(host::type_error(&format!("{} is not a function", name)))
5927        }
5928    }
5929}
5930
5931/// A copy of the whole backing store, for the methods that genuinely consume
5932/// every element (`map`, `filter`, `join`, …). Never call it just to read
5933/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
5934fn array_items(recv: &Value) -> Vec<Value> {
5935    with_host(|h| match h.get(recv) {
5936        Some(JsObj::Array(items)) => items.clone(),
5937        _ => Vec::new(),
5938    })
5939}
5940
5941/// The ELIDED positions of array `recv` as a membership set. A dense array —
5942/// which is nearly every array — answers with an empty set after a single
5943/// negative hash probe and allocates nothing.
5944///
5945/// The iteration methods split into two groups, and the split is not a matter of
5946/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
5947/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
5948/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
5949/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
5950fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
5951    with_host(|h| h.hole_indices(recv)).into_iter().collect()
5952}
5953
5954/// The element count, without copying the elements.
5955fn array_len(recv: &Value) -> usize {
5956    peek(recv, |o| match o {
5957        JsObj::Array(items) => Some(items.len()),
5958        _ => None,
5959    })
5960    .unwrap_or(0)
5961}
5962
5963fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
5964    array_method_on(recv, recv, name, args)
5965}
5966
5967/// The `Array.prototype` methods that WRITE to their receiver, and so need the
5968/// generic path to copy the result back onto the array-like.
5969const ARRAY_MUTATORS: &[&str] = &[
5970    "push",
5971    "pop",
5972    "shift",
5973    "unshift",
5974    "splice",
5975    "sort",
5976    "reverse",
5977    "fill",
5978    "copyWithin",
5979];
5980
5981/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
5982/// a DOM-ish collection, `arguments`).
5983///
5984/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
5985/// rather than over an Array's element vector, so the receiver only has to have
5986/// a `length`. The elements are read out into a temporary Array, the ordinary
5987/// implementation runs on that, and a MUTATING method writes the result back —
5988/// which keeps one implementation of each method rather than a second, generic
5989/// one that could drift from it.
5990///
5991/// An index the receiver does not own is a HOLE in the temporary, so the
5992/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
5993fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
5994    let len = match get_property(recv, "length") {
5995        Ok(v) => host::to_array_length(&v).unwrap_or(0),
5996        Err(_) => 0,
5997    };
5998    // A STRING receiver owns every index of its length; `has_property` answers
5999    // for objects and reports none of them, which made `[].map.call('abc', f)`
6000    // an array of three holes.
6001    let dense = with_host(|h| h.as_str(recv)).is_some();
6002    let mut items = Vec::with_capacity(len);
6003    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
6004    for i in 0..len {
6005        let k = i.to_string();
6006        if dense || has_property(recv, &k)? {
6007            items.push(get_property(recv, &k)?);
6008        } else {
6009            holes.insert(i);
6010            items.push(Value::Undef);
6011        }
6012    }
6013    let tmp = with_host(|h| {
6014        let a = h.new_array(items);
6015        h.install_holes(&a, holes);
6016        a
6017    });
6018    let out = array_method_on(&tmp, recv, method, args)?;
6019    if ARRAY_MUTATORS.contains(&method) {
6020        let result = with_host(|h| match h.get(&tmp) {
6021            Some(JsObj::Array(items)) => items.clone(),
6022            _ => Vec::new(),
6023        });
6024        for (i, v) in result.iter().enumerate() {
6025            set_property(recv, &i.to_string(), v.clone())?;
6026        }
6027        set_property(recv, "length", Value::Float(result.len() as f64))?;
6028    }
6029    Ok(out)
6030}
6031
6032/// `Array.prototype.<name>` on `recv`.
6033///
6034/// `this_value` is what a callback receives as its third argument and what a
6035/// mutating method returns — the same object as `recv` for an ordinary array
6036/// call, but the ORIGINAL array-like when `array_generic` runs a method against
6037/// a temporary copy (`Array.prototype.slice.call(arguments)`).
6038fn array_method_on(
6039    recv: &Value,
6040    this_value: &Value,
6041    name: &str,
6042    args: Vec<Value>,
6043) -> Result<Value, String> {
6044    match name {
6045        "push" => {
6046            // `push` returns the new length; take it from the same mutable
6047            // borrow rather than copying the array back out to count it.
6048            let len = with_host(|h| {
6049                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6050                    items.extend(args.iter().cloned());
6051                    items.len()
6052                } else {
6053                    0
6054                }
6055            });
6056            Ok(Value::Float(len as f64))
6057        }
6058        "pop" => Ok(with_host(|h| {
6059            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6060                items.pop().unwrap_or(Value::Undef)
6061            } else {
6062                Value::Undef
6063            };
6064            let len = match h.get(recv) {
6065                Some(JsObj::Array(items)) => items.len(),
6066                _ => 0,
6067            };
6068            h.truncate_holes(recv, len);
6069            popped
6070        })),
6071        "shift" => Ok(with_host(|h| {
6072            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6073                if items.is_empty() {
6074                    Value::Undef
6075                } else {
6076                    items.remove(0)
6077                }
6078            } else {
6079                Value::Undef
6080            };
6081            h.remap_holes(recv, |i| i.checked_sub(1));
6082            shifted
6083        })),
6084        "unshift" => {
6085            with_host(|h| {
6086                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6087                    for (i, a) in args.iter().enumerate() {
6088                        items.insert(i, a.clone());
6089                    }
6090                }
6091                let n = args.len();
6092                h.remap_holes(recv, |i| Some(i + n));
6093            });
6094            Ok(Value::Float(array_len(recv) as f64))
6095        }
6096        "join" => {
6097            let sep = if args.is_empty() {
6098                ",".to_string()
6099            } else {
6100                with_host(|h| h.str_of(&args[0]))
6101            };
6102            join_array(recv, &sep)
6103        }
6104        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
6105        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
6106        // empty string. It threw `is not a function` — the whole method was
6107        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
6108        "toLocaleString" => {
6109            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
6110            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
6111            if !host::join_stack_push(recv) {
6112                return Ok(with_host(|h| h.new_str(String::new())));
6113            }
6114            let items = array_items(recv);
6115            let mut parts: Vec<String> = Vec::with_capacity(items.len());
6116            for it in &items {
6117                if with_host(|h| h.is_nullish(it)) {
6118                    parts.push(String::new());
6119                    continue;
6120                }
6121                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
6122                    Ok(v) => v,
6123                    Err(e) => {
6124                        host::join_stack_pop();
6125                        return Err(e);
6126                    }
6127                };
6128                parts.push(with_host(|h| h.str_of(&v)));
6129            }
6130            host::join_stack_pop();
6131            Ok(with_host(|h| h.new_str(parts.join(","))))
6132        }
6133        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
6134        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
6135        // `Get`-based `includes` reports `true` for the same array.
6136        "indexOf" => {
6137            let items = array_items(recv);
6138            let holes = hole_set(recv);
6139            let target = arg0(&args);
6140            let idx = with_host(|h| {
6141                items
6142                    .iter()
6143                    .enumerate()
6144                    .position(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
6145            });
6146            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
6147        }
6148        "lastIndexOf" => {
6149            let items = array_items(recv);
6150            let holes = hole_set(recv);
6151            let target = arg0(&args);
6152            let idx = with_host(|h| {
6153                items
6154                    .iter()
6155                    .enumerate()
6156                    .rposition(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
6157            });
6158            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
6159        }
6160        "includes" => {
6161            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
6162            let items = array_items(recv);
6163            let target = arg0(&args);
6164            let tnan = matches!(target, Value::Float(f) if f.is_nan());
6165            Ok(Value::Bool(with_host(|h| {
6166                items.iter().any(|x| {
6167                    (tnan && matches!(x, Value::Float(f) if f.is_nan())) || h.strict_eq(x, &target)
6168                })
6169            })))
6170        }
6171        "slice" => {
6172            let items = array_items(recv);
6173            let (lo, hi) = slice_bounds(&args, items.len());
6174            Ok(with_host(|h| {
6175                let out = h.new_array(items[lo..hi].to_vec());
6176                h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo));
6177                out
6178            }))
6179        }
6180        "concat" => {
6181            let mut out = array_items(recv);
6182            // A hole in either the receiver or a spreadable argument stays a hole
6183            // in the result, at its shifted position.
6184            let mut holes = hole_set(recv);
6185            let mut sources: Vec<(Value, usize)> = Vec::new();
6186            for a in &args {
6187                match with_host(|h| h.get(a).cloned()) {
6188                    Some(JsObj::Array(items)) => {
6189                        sources.push((a.clone(), out.len()));
6190                        out.extend(items);
6191                    }
6192                    _ => out.push(a.clone()),
6193                }
6194            }
6195            for (src, base) in sources {
6196                holes.extend(
6197                    with_host(|h| h.hole_indices(&src))
6198                        .into_iter()
6199                        .map(|i| i + base),
6200                );
6201            }
6202            Ok(with_host(|h| {
6203                let arr = h.new_array(out);
6204                h.install_holes(&arr, holes);
6205                arr
6206            }))
6207        }
6208        "reverse" => {
6209            let len = array_len(recv);
6210            with_host(|h| {
6211                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6212                    items.reverse();
6213                }
6214                h.remap_holes(recv, |i| Some(len - 1 - i));
6215            });
6216            Ok(this_value.clone())
6217        }
6218        "fill" => {
6219            // fill(value[, start[, end]]) — negative indices count from the end.
6220            let val = arg0(&args);
6221            let len = array_len(recv) as i64;
6222            let norm =
6223                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
6224            let start = if args.len() >= 2 {
6225                norm(arg_num(&args, 1) as i64)
6226            } else {
6227                0
6228            };
6229            let end = if args.len() >= 3 {
6230                norm(arg_num(&args, 2) as i64)
6231            } else {
6232                len as usize
6233            };
6234            with_host(|h| {
6235                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6236                    for it in items.iter_mut().take(end).skip(start) {
6237                        *it = val.clone();
6238                    }
6239                }
6240                // Every filled position now holds a real value.
6241                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
6242            });
6243            Ok(this_value.clone())
6244        }
6245        "copyWithin" => {
6246            // copyWithin(target, start[, end]) — copy a slice within the array.
6247            let items = array_items(recv);
6248            let len = items.len() as i64;
6249            let norm =
6250                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
6251            let target = norm(arg_num(&args, 0) as i64);
6252            let start = if args.len() >= 2 {
6253                norm(arg_num(&args, 1) as i64)
6254            } else {
6255                0
6256            };
6257            let end = if args.len() >= 3 {
6258                norm(arg_num(&args, 2) as i64)
6259            } else {
6260                len as usize
6261            };
6262            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
6263            let copied = slice.len();
6264            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
6265            // deletes the target when the source has no such property);
6266            // everything outside the written range keeps its own.
6267            let src_holes = hole_set(recv);
6268            with_host(|h| {
6269                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
6270                    for (k, v) in slice.into_iter().enumerate() {
6271                        if target + k < a.len() {
6272                            a[target + k] = v;
6273                        }
6274                    }
6275                }
6276                let len = len as usize;
6277                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
6278                    .iter()
6279                    .copied()
6280                    .filter(|i| *i < target || *i >= (target + copied).min(len))
6281                    .collect();
6282                for k in 0..copied {
6283                    if target + k < len && src_holes.contains(&(start + k)) {
6284                        holes.insert(target + k);
6285                    }
6286                }
6287                h.install_holes(recv, holes);
6288            });
6289            Ok(this_value.clone())
6290        }
6291        "at" => {
6292            let items = array_items(recv);
6293            let mut i = arg_num(&args, 0) as i64;
6294            if i < 0 {
6295                i += items.len() as i64;
6296            }
6297            Ok(if i >= 0 && (i as usize) < items.len() {
6298                items[i as usize].clone()
6299            } else {
6300                Value::Undef
6301            })
6302        }
6303        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
6304        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
6305        // twice and yields `[2, <1 empty item>, 6]`.
6306        "map" => {
6307            let items = array_items(recv);
6308            let holes = hole_set(recv);
6309            let cb = arg0(&args);
6310            let mut out = Vec::with_capacity(items.len());
6311            for (i, it) in items.iter().enumerate() {
6312                if holes.contains(&i) {
6313                    out.push(Value::Undef);
6314                    continue;
6315                }
6316                out.push(host::invoke(
6317                    &cb,
6318                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6319                    None,
6320                )?);
6321            }
6322            Ok(with_host(|h| {
6323                let arr = h.new_array(out);
6324                h.install_holes(&arr, holes);
6325                arr
6326            }))
6327        }
6328        "flatMap" => {
6329            let items = array_items(recv);
6330            let cb = arg0(&args);
6331            let holes = hole_set(recv);
6332            let mut out = Vec::new();
6333            for (i, it) in items.iter().enumerate() {
6334                if holes.contains(&i) {
6335                    continue;
6336                }
6337                let r = host::invoke(
6338                    &cb,
6339                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6340                    None,
6341                )?;
6342                match with_host(|h| h.get(&r).cloned()) {
6343                    Some(JsObj::Array(inner)) => out.extend(inner),
6344                    _ => out.push(r),
6345                }
6346            }
6347            Ok(with_host(|h| h.new_array(out)))
6348        }
6349        "filter" => {
6350            let items = array_items(recv);
6351            let holes = hole_set(recv);
6352            let cb = arg0(&args);
6353            let mut out = Vec::new();
6354            for (i, it) in items.iter().enumerate() {
6355                if holes.contains(&i) {
6356                    continue;
6357                }
6358                let keep = host::invoke(
6359                    &cb,
6360                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6361                    None,
6362                )?;
6363                if with_host(|h| h.truthy(&keep)) {
6364                    out.push(it.clone());
6365                }
6366            }
6367            Ok(with_host(|h| h.new_array(out)))
6368        }
6369        "forEach" => {
6370            let items = array_items(recv);
6371            let holes = hole_set(recv);
6372            let cb = arg0(&args);
6373            for (i, it) in items.iter().enumerate() {
6374                if holes.contains(&i) {
6375                    continue;
6376                }
6377                host::invoke(
6378                    &cb,
6379                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6380                    None,
6381                )?;
6382            }
6383            Ok(Value::Undef)
6384        }
6385        "find" => {
6386            let items = array_items(recv);
6387            let cb = arg0(&args);
6388            for (i, it) in items.iter().enumerate() {
6389                let m = host::invoke(
6390                    &cb,
6391                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6392                    None,
6393                )?;
6394                if with_host(|h| h.truthy(&m)) {
6395                    return Ok(it.clone());
6396                }
6397            }
6398            Ok(Value::Undef)
6399        }
6400        "findIndex" => {
6401            let items = array_items(recv);
6402            let cb = arg0(&args);
6403            for (i, it) in items.iter().enumerate() {
6404                let m = host::invoke(
6405                    &cb,
6406                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6407                    None,
6408                )?;
6409                if with_host(|h| h.truthy(&m)) {
6410                    return Ok(Value::Float(i as f64));
6411                }
6412            }
6413            Ok(Value::Float(-1.0))
6414        }
6415        "some" => {
6416            let items = array_items(recv);
6417            let holes = hole_set(recv);
6418            let cb = arg0(&args);
6419            for (i, it) in items.iter().enumerate() {
6420                if holes.contains(&i) {
6421                    continue;
6422                }
6423                let m = host::invoke(
6424                    &cb,
6425                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6426                    None,
6427                )?;
6428                if with_host(|h| h.truthy(&m)) {
6429                    return Ok(Value::Bool(true));
6430                }
6431            }
6432            Ok(Value::Bool(false))
6433        }
6434        "every" => {
6435            let items = array_items(recv);
6436            let holes = hole_set(recv);
6437            let cb = arg0(&args);
6438            for (i, it) in items.iter().enumerate() {
6439                if holes.contains(&i) {
6440                    continue;
6441                }
6442                let m = host::invoke(
6443                    &cb,
6444                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6445                    None,
6446                )?;
6447                if !with_host(|h| h.truthy(&m)) {
6448                    return Ok(Value::Bool(false));
6449                }
6450            }
6451            Ok(Value::Bool(true))
6452        }
6453        "reduce" => {
6454            let items = array_items(recv);
6455            let holes = hole_set(recv);
6456            let cb = arg0(&args);
6457            let mut acc;
6458            let mut start = 0;
6459            if args.len() >= 2 {
6460                acc = args[1].clone();
6461            } else {
6462                // With no seed the accumulator is the first PRESENT element, so a
6463                // leading run of holes is skipped rather than seeding `undefined`.
6464                match (0..items.len()).find(|i| !holes.contains(i)) {
6465                    Some(i) => {
6466                        acc = items[i].clone();
6467                        start = i + 1;
6468                    }
6469                    None => {
6470                        return Err(host::type_error(
6471                            "Reduce of empty array with no initial value",
6472                        ))
6473                    }
6474                }
6475            }
6476            for (i, it) in items.iter().enumerate().skip(start) {
6477                if holes.contains(&i) {
6478                    continue;
6479                }
6480                acc = host::invoke(
6481                    &cb,
6482                    vec![acc, it.clone(), Value::Float(i as f64), this_value.clone()],
6483                    None,
6484                )?;
6485            }
6486            Ok(acc)
6487        }
6488        "reduceRight" => {
6489            let items = array_items(recv);
6490            let holes = hole_set(recv);
6491            let cb = arg0(&args);
6492            let n = items.len();
6493            let mut acc;
6494            let mut i = n; // one past the next index to process (walking down)
6495            if args.len() >= 2 {
6496                acc = args[1].clone();
6497            } else {
6498                match (0..n).rev().find(|i| !holes.contains(i)) {
6499                    Some(k) => {
6500                        acc = items[k].clone();
6501                        i = k;
6502                    }
6503                    None => {
6504                        return Err(host::type_error(
6505                            "Reduce of empty array with no initial value",
6506                        ))
6507                    }
6508                }
6509            }
6510            while i > 0 {
6511                i -= 1;
6512                if holes.contains(&i) {
6513                    continue;
6514                }
6515                acc = host::invoke(
6516                    &cb,
6517                    vec![
6518                        acc,
6519                        items[i].clone(),
6520                        Value::Float(i as f64),
6521                        this_value.clone(),
6522                    ],
6523                    None,
6524                )?;
6525            }
6526            Ok(acc)
6527        }
6528        "findLast" => {
6529            let items = array_items(recv);
6530            let cb = arg0(&args);
6531            for i in (0..items.len()).rev() {
6532                let m = host::invoke(
6533                    &cb,
6534                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
6535                    None,
6536                )?;
6537                if with_host(|h| h.truthy(&m)) {
6538                    return Ok(items[i].clone());
6539                }
6540            }
6541            Ok(Value::Undef)
6542        }
6543        "findLastIndex" => {
6544            let items = array_items(recv);
6545            let cb = arg0(&args);
6546            for i in (0..items.len()).rev() {
6547                let m = host::invoke(
6548                    &cb,
6549                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
6550                    None,
6551                )?;
6552                if with_host(|h| h.truthy(&m)) {
6553                    return Ok(Value::Float(i as f64));
6554                }
6555            }
6556            Ok(Value::Float(-1.0))
6557        }
6558        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
6559        // and the holes are re-created at the tail — `[3,,1].sort()` is
6560        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
6561        "sort" => {
6562            let all = array_items(recv);
6563            let holes = hole_set(recv);
6564            let mut items: Vec<Value> = all
6565                .iter()
6566                .enumerate()
6567                .filter(|(i, _)| !holes.contains(i))
6568                .map(|(_, v)| v.clone())
6569                .collect();
6570            sort_values(&mut items, args.first())?;
6571            let present = items.len();
6572            items.resize(all.len(), Value::Undef);
6573            with_host(|h| {
6574                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
6575                    *a = items;
6576                }
6577                h.install_holes(recv, (present..all.len()).collect());
6578            });
6579            Ok(this_value.clone())
6580        }
6581        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
6582        "toSorted" => {
6583            let mut items = array_items(recv);
6584            sort_values(&mut items, args.first())?;
6585            Ok(with_host(|h| h.new_array(items)))
6586        }
6587        "toReversed" => {
6588            let mut items = array_items(recv);
6589            items.reverse();
6590            Ok(with_host(|h| h.new_array(items)))
6591        }
6592        "toSpliced" => {
6593            let mut items = array_items(recv);
6594            let len = items.len();
6595            let start = {
6596                let s = arg_num(&args, 0);
6597                if s < 0.0 {
6598                    ((len as f64 + s).max(0.0)) as usize
6599                } else {
6600                    (s as usize).min(len)
6601                }
6602            };
6603            let delete = if args.len() >= 2 {
6604                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
6605            } else if args.is_empty() {
6606                0
6607            } else {
6608                len - start
6609            };
6610            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
6611            items.splice(start..start + delete, inserts);
6612            Ok(with_host(|h| h.new_array(items)))
6613        }
6614        "with" => {
6615            let mut items = array_items(recv);
6616            let len = items.len() as i64;
6617            let rel = arg_num(&args, 0) as i64;
6618            let idx = if rel < 0 { len + rel } else { rel };
6619            if idx < 0 || idx >= len {
6620                return Err(host::range_error(&format!("Invalid index : {rel}")));
6621            }
6622            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
6623            Ok(with_host(|h| h.new_array(items)))
6624        }
6625        "flat" => {
6626            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
6627            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
6628            let raw = if args.is_empty() {
6629                1.0
6630            } else {
6631                arg_num(&args, 0)
6632            };
6633            let depth = if raw.is_nan() {
6634                0.0
6635            } else if raw.is_infinite() {
6636                raw
6637            } else {
6638                raw.trunc()
6639            };
6640            let mut out = Vec::new();
6641            flatten_into(recv, depth, &mut out)?;
6642            Ok(with_host(|h| h.new_array(out)))
6643        }
6644        "keys" => {
6645            let n = array_len(recv);
6646            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
6647            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6648        }
6649        "values" | "@@iterator" => {
6650            let items = array_items(recv);
6651            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6652        }
6653        "entries" => {
6654            let items = array_items(recv);
6655            let pairs: Vec<Value> = items
6656                .into_iter()
6657                .enumerate()
6658                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
6659                .collect();
6660            Ok(with_host(|h| {
6661                h.alloc(JsObj::Iter {
6662                    items: pairs,
6663                    idx: 0,
6664                })
6665            }))
6666        }
6667        "splice" => array_splice(recv, args),
6668        // `Array.prototype.toString` IS `join()` with the default separator
6669        // (23.1.3.36), so it converts each element with `ToString` too — and
6670        // shares its cycle cut, which is the whole reason it must not call
6671        // `join_parts` directly: `ToString` of a nested array lands back here.
6672        "toString" => join_array(recv, ","),
6673        // An Array inherits from `Object.prototype` too, so the methods it does
6674        // not override resolve there. `[].hasOwnProperty` already read back as a
6675        // function through the property path, but CALLING it landed here and
6676        // threw `is not a function`.
6677        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
6678        _ => Err(host::type_error(&format!("{name} is not a function"))),
6679    }
6680}
6681
6682/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
6683/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
6684/// cut, which is not optional here: `ToString` of an element that is itself an
6685/// array re-enters through `toString`, so guarding only `join` left
6686/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
6687/// process. On node v26.7.0 that expression is `""`.
6688fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
6689    if !host::join_stack_push(recv) {
6690        return Ok(with_host(|h| h.new_str(String::new())));
6691    }
6692    let parts = join_parts(&array_items(recv));
6693    host::join_stack_pop();
6694    let s = parts?.join(sep);
6695    Ok(with_host(|h| h.new_str(s)))
6696}
6697
6698/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
6699/// `null`/`undefined` element contributes the empty string, every other element
6700/// is `ToString(element)` — which for an object means invoking its `toString`,
6701/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
6702/// `"[object Object]"`.
6703///
6704/// The all-primitive array — the overwhelmingly common one — is rendered under
6705/// a single host borrow; only an array actually holding an object pays for the
6706/// re-entrant per-element conversion.
6707fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
6708    let fast = with_host(|h| {
6709        items
6710            .iter()
6711            .map(|x| match x {
6712                Value::Undef => Some(String::new()),
6713                _ if h.is_null(x) => Some(String::new()),
6714                // A SYMBOL element is primitive but has no `ToString`, so it must
6715                // fall through to the fallible path and throw there:
6716                // `[Symbol()].join()` is a TypeError on node v26.7.0.
6717                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
6718                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
6719                _ => None,
6720            })
6721            .collect::<Vec<_>>()
6722    });
6723    if fast.iter().all(Option::is_some) {
6724        return Ok(fast.into_iter().flatten().collect());
6725    }
6726    let mut out = Vec::with_capacity(items.len());
6727    for (x, p) in items.iter().zip(fast) {
6728        match p {
6729            Some(s) => out.push(s),
6730            None => {
6731                let s = host::to_string_value(x)?;
6732                out.push(with_host(|h| h.str_of(&s)));
6733            }
6734        }
6735    }
6736    Ok(out)
6737}
6738
6739/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
6740/// sort — O(n log n) comparisons — with the fallible JS comparator called from
6741/// the merge step; default order is by the string form of each element.
6742/// Propagates a comparator error.
6743///
6744/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
6745/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
6746/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
6747/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
6748/// number of times it is called is.
6749pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
6750    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
6751    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
6752    // invoke path and reporting the generic `null is not a function`.
6753    let cmp = match cmp {
6754        Some(Value::Undef) => None,
6755        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
6756            let shown = with_host(|h| h.inspect(v));
6757            return Err(host::type_error(&format!(
6758                "The comparison function must be either a function or undefined: {shown}"
6759            )));
6760        }
6761        other => other,
6762    };
6763    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
6764    // comparator — it sorts to the end after the defined values are ordered.
6765    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
6766    // node v26.7.0; the insertion sort called the comparator twice, on
6767    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
6768    // is `undefined`, so swapping keeps the defined values in input order.
6769    let mut defined = 0;
6770    for i in 0..items.len() {
6771        if !matches!(items[i], Value::Undef) {
6772            items.swap(defined, i);
6773            defined += 1;
6774        }
6775    }
6776    merge_sort(&mut items[..defined], cmp)
6777}
6778
6779/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
6780/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
6781/// `> 0` and the pair keeps its input order.
6782fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
6783    match cmp {
6784        Some(cb) => {
6785            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
6786            Ok(with_host(|h| h.to_number(&v)))
6787        }
6788        None => {
6789            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
6790            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
6791            // from Rust's `String` order off the BMP.
6792            let x = with_host(|h| h.str_of(a));
6793            let y = with_host(|h| h.str_of(b));
6794            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
6795                Ok(1.0)
6796            } else {
6797                Ok(-1.0)
6798            }
6799        }
6800    }
6801}
6802
6803/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
6804/// array cannot walk the native stack the JS comparator also runs on, and the
6805/// two buffers are swapped each pass instead of copied back.
6806fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
6807    let n = items.len();
6808    if n < 2 {
6809        return Ok(());
6810    }
6811    let mut src = items.to_vec();
6812    let mut dst = src.clone();
6813    let mut width = 1;
6814    while width < n {
6815        let mut lo = 0;
6816        while lo < n {
6817            let mid = (lo + width).min(n);
6818            let hi = (lo + 2 * width).min(n);
6819            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
6820            lo = hi;
6821        }
6822        std::mem::swap(&mut src, &mut dst);
6823        width *= 2;
6824    }
6825    items.clone_from_slice(&src);
6826    Ok(())
6827}
6828
6829/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
6830/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
6831/// keeps the two `k:0` entries in input order, as node does.
6832fn merge(
6833    left: &[Value],
6834    right: &[Value],
6835    out: &mut [Value],
6836    cmp: Option<&Value>,
6837) -> Result<(), String> {
6838    let (mut i, mut j, mut k) = (0, 0, 0);
6839    while i < left.len() && j < right.len() {
6840        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
6841            out[k] = right[j].clone();
6842            j += 1;
6843        } else {
6844            out[k] = left[i].clone();
6845            i += 1;
6846        }
6847        k += 1;
6848    }
6849    for v in left[i..].iter().chain(&right[j..]) {
6850        out[k] = v.clone();
6851        k += 1;
6852    }
6853    Ok(())
6854}
6855
6856/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
6857/// f64 so `Infinity` (full flatten) and finite counts share one path.
6858///
6859/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
6860/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
6861/// exceeded` on node v26.7.0. That is reproduced by checking the same native
6862/// stack floor the VM does, so the answer is a catchable error rather than the
6863/// `fatal runtime error: stack overflow` abort this used to produce.
6864/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
6865/// elements because each level tests `HasProperty` before recursing, so a hole
6866/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
6867fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
6868    if host::stack_exhausted() {
6869        return Err(host::stack_overflow_error());
6870    }
6871    let items = array_items(src);
6872    let holes = hole_set(src);
6873    for (i, it) in items.into_iter().enumerate() {
6874        if holes.contains(&i) {
6875            continue;
6876        }
6877        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
6878        if nested {
6879            flatten_into(&it, depth - 1.0, out)?;
6880        } else {
6881            out.push(it);
6882        }
6883    }
6884    Ok(())
6885}
6886
6887fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
6888    let len = array_len(recv);
6889    let start = {
6890        let s = arg_num(&args, 0);
6891        if s < 0.0 {
6892            ((len as f64 + s).max(0.0)) as usize
6893        } else {
6894            (s as usize).min(len)
6895        }
6896    };
6897    let delete = if args.len() >= 2 {
6898        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
6899    } else {
6900        len - start
6901    };
6902    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
6903    let inserted = inserts.len();
6904    // The receiver's holes shift by (inserted - deleted) past the cut, and the
6905    // ones inside the cut move into the RETURNED array at their offset there.
6906    let holes = hole_set(recv);
6907    let removed = with_host(|h| {
6908        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6909            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
6910            removed
6911        } else {
6912            Vec::new()
6913        }
6914    });
6915    Ok(with_host(|h| {
6916        h.install_holes(
6917            recv,
6918            holes
6919                .iter()
6920                .filter_map(|&i| {
6921                    if i < start {
6922                        Some(i)
6923                    } else if i < start + delete {
6924                        None
6925                    } else {
6926                        Some(i - delete + inserted)
6927                    }
6928                })
6929                .collect(),
6930        );
6931        let out = h.new_array(removed);
6932        h.install_holes(
6933            &out,
6934            holes
6935                .iter()
6936                .filter(|&&i| i >= start && i < start + delete)
6937                .map(|&i| i - start)
6938                .collect(),
6939        );
6940        out
6941    }))
6942}
6943
6944fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
6945    let norm = |v: f64| -> usize {
6946        if v < 0.0 {
6947            ((len as f64 + v).max(0.0)) as usize
6948        } else {
6949            (v as usize).min(len)
6950        }
6951    };
6952    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
6953        0
6954    } else {
6955        norm(arg_num(args, 0))
6956    };
6957    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
6958        len
6959    } else {
6960        norm(arg_num(args, 1))
6961    };
6962    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
6963    // never a reversed one: JS `slice` clamps `end` up to `start`.
6964    (lo, hi.max(lo))
6965}
6966
6967fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
6968    // Every index-bearing method below counts UTF-16 code units, so they all
6969    // work off this one decoding rather than off `s.chars()` (code points),
6970    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
6971    let u = crate::utf16::Units::of(s);
6972    match name {
6973        // `for…of` / spread over a string iterates CODE POINTS, not code units:
6974        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
6975        // is the one string operation that is specified in chars, so it stays
6976        // on `s.chars()` on purpose — do not "fix" it to match the others.
6977        "@@iterator" => {
6978            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
6979            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6980        }
6981        "toUpperCase" => Ok(new_s(s.to_uppercase())),
6982        "toLowerCase" => Ok(new_s(s.to_lowercase())),
6983        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
6984        // from the plain forms only for the locale-specific mappings (Turkish
6985        // dotless i, Lithuanian accents); with no locale argument they are the
6986        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
6987        // `to_lowercase`. They threw `is not a function` before, so the common
6988        // no-argument call — the only form this runtime can answer, since it
6989        // carries no ICU — failed outright rather than agreeing with node.
6990        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
6991        // is `'i'` here and `'ı'` in node.
6992        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
6993        // has no locale rendering. Missing it made an ARRAY of strings fail too,
6994        // since `Array.prototype.toLocaleString` invokes it per element.
6995        "toLocaleString" => Ok(new_s(s.to_string())),
6996        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
6997        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
6998        // Locale comparison (ASCII approximation of ICU collation): primary by
6999        // case-folded order, then lowercase sorts before uppercase at a tie.
7000        "localeCompare" => {
7001            let other = with_host(|h| h.str_of(&arg0(&args)));
7002            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
7003            let r = match la.cmp(&lb) {
7004                std::cmp::Ordering::Less => -1.0,
7005                std::cmp::Ordering::Greater => 1.0,
7006                std::cmp::Ordering::Equal => {
7007                    let mut t = 0.0;
7008                    for (ca, cb) in s.chars().zip(other.chars()) {
7009                        if ca != cb {
7010                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
7011                            break;
7012                        }
7013                    }
7014                    t
7015                }
7016            };
7017            Ok(Value::Float(r))
7018        }
7019        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
7020        //
7021        // This used to return the receiver unchanged and only validate the FORM
7022        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
7023        // one code point) and `"Å"` (NFD, two) stayed distinct under
7024        // `.normalize()`, so the standard way to compare Unicode text for
7025        // canonical equivalence silently answered `false`, and `NFKC` never
7026        // folded a compatibility character (`"fi"` stayed one code point instead
7027        // of becoming `"fi"`). The tables come from `unicode-normalization`.
7028        "normalize" => {
7029            use unicode_normalization::UnicodeNormalization;
7030            let form = match args.first() {
7031                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
7032                _ => "NFC".to_string(),
7033            };
7034            let out = match form.as_str() {
7035                "NFC" => s.nfc().collect::<String>(),
7036                "NFD" => s.nfd().collect::<String>(),
7037                "NFKC" => s.nfkc().collect::<String>(),
7038                "NFKD" => s.nfkd().collect::<String>(),
7039                _ => {
7040                    return Err(host::range_error(
7041                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
7042                    ))
7043                }
7044            };
7045            Ok(new_s(out))
7046        }
7047        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
7048        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
7049        // value this runtime can hold is well-formed by construction and
7050        // `toWellFormed` has nothing to replace. Both answers are therefore
7051        // exact for every string that survives storage; the one case node
7052        // answers differently is a surrogate half extracted by `charAt`/`slice`,
7053        // which is already `U+FFFD` here — the documented lone-surrogate
7054        // boundary in `utf16`, not a separate gap.
7055        "isWellFormed" => Ok(Value::Bool(true)),
7056        "toWellFormed" => Ok(new_s(s.to_string())),
7057        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
7058        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
7059        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
7060        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
7061        "toString" | "valueOf" => Ok(new_s(s.to_string())),
7062        "charAt" => {
7063            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
7064            Ok(new_s(at.unwrap_or_default()))
7065        }
7066        "at" => {
7067            let n = arg_num(&args, 0);
7068            // A negative position counts back from the end; `NaN` is 0. An
7069            // infinite position is out of range in either direction.
7070            let i = if n.is_nan() {
7071                Some(0i64)
7072            } else if n.is_finite() {
7073                let i = n.trunc() as i64;
7074                Some(if i < 0 { i + u.len() as i64 } else { i })
7075            } else {
7076                None
7077            };
7078            match i
7079                .and_then(|i| usize::try_from(i).ok())
7080                .and_then(|i| u.unit_str(i))
7081            {
7082                Some(c) => Ok(new_s(c)),
7083                None => Ok(Value::Undef),
7084            }
7085        }
7086        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
7087        // astral character, not the character. `codePointAt` looks ahead one
7088        // unit and reports the whole scalar when the pair is well formed. They
7089        // agree everywhere on the BMP, which is why they used to share an arm.
7090        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
7091        // `codePointAt` yields `undefined` (measured on node v26.7.0).
7092        "charCodeAt" => {
7093            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
7094            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
7095        }
7096        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
7097            Some(cp) => Ok(Value::Float(f64::from(cp))),
7098            None => Ok(Value::Undef),
7099        },
7100        // The search quartet all honor their optional position argument.
7101        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
7102        // parameterCount walks a query string with exactly that call.
7103        "indexOf" => {
7104            let needle = needle_units(&args);
7105            let from = clamp_pos(arg_num(&args, 1), u.len());
7106            Ok(Value::Float(
7107                search_from(u.as_slice(), needle.as_slice(), from)
7108                    .map(|i| i as f64)
7109                    .unwrap_or(-1.0),
7110            ))
7111        }
7112        "lastIndexOf" => {
7113            let needle = needle_units(&args);
7114            // An absent or NaN position means "search the whole string".
7115            let n = arg_num(&args, 1);
7116            let upto = if n.is_nan() {
7117                u.len()
7118            } else {
7119                clamp_pos(n, u.len())
7120            };
7121            Ok(Value::Float(
7122                search_last(u.as_slice(), needle.as_slice(), upto)
7123                    .map(|i| i as f64)
7124                    .unwrap_or(-1.0),
7125            ))
7126        }
7127        "includes" => {
7128            let needle = needle_units(&args);
7129            let from = clamp_pos(arg_num(&args, 1), u.len());
7130            Ok(Value::Bool(
7131                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
7132            ))
7133        }
7134        "startsWith" => {
7135            let needle = needle_units(&args);
7136            let from = clamp_pos(arg_num(&args, 1), u.len());
7137            Ok(Value::Bool(
7138                u.as_slice()[from..].starts_with(needle.as_slice()),
7139            ))
7140        }
7141        "endsWith" => {
7142            let needle = needle_units(&args);
7143            // The 2nd argument is where the string is treated as ENDING.
7144            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
7145                u.len()
7146            } else {
7147                clamp_pos(arg_num(&args, 1), u.len())
7148            };
7149            Ok(Value::Bool(
7150                u.as_slice()[..end].ends_with(needle.as_slice()),
7151            ))
7152        }
7153        "slice" => {
7154            let (lo, hi) = slice_bounds(&args, u.len());
7155            Ok(new_s(u.slice(lo, hi)))
7156        }
7157        "substring" => {
7158            let mut a = arg_num(&args, 0).max(0.0) as usize;
7159            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
7160                u.len()
7161            } else {
7162                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
7163            };
7164            a = a.min(u.len());
7165            if a > b {
7166                std::mem::swap(&mut a, &mut b);
7167            }
7168            Ok(new_s(u.slice(a, b)))
7169        }
7170        "substr" => {
7171            // A negative start counts from the end: max(len + start, 0).
7172            let len = u.len() as i64;
7173            let mut start = arg_num(&args, 0) as i64;
7174            if start < 0 {
7175                start = (len + start).max(0);
7176            }
7177            let start = (start as usize).min(u.len());
7178            let count = if args.len() >= 2 {
7179                arg_num(&args, 1).max(0.0) as usize
7180            } else {
7181                u.len()
7182            };
7183            let end = start.saturating_add(count).min(u.len());
7184            Ok(new_s(u.slice(start, end)))
7185        }
7186        "repeat" => {
7187            let n = arg_num(&args, 0);
7188            // `RangeError`, not `TypeError`, and the count is named:
7189            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
7190            if n < 0.0 || !n.is_finite() {
7191                return Err(host::range_error(&format!(
7192                    "Invalid count value: {}",
7193                    host::fmt_number(n)
7194                )));
7195            }
7196            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
7197            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
7198            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
7199            // is `RangeError: Invalid string length`.
7200            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
7201                return Err(host::invalid_string_length());
7202            }
7203            Ok(new_s(s.repeat(n as usize)))
7204        }
7205        "concat" => {
7206            let mut out = s.to_string();
7207            for a in &args {
7208                out.push_str(&with_host(|h| h.str_of(a)));
7209            }
7210            Ok(new_s(out))
7211        }
7212        "padStart" => Ok(new_s(pad(s, &args, true)?)),
7213        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
7214        // Regex-taking string methods: dispatch to the regexp module when the
7215        // argument is a RegExp; otherwise keep the plain-string behavior.
7216        "match" => crate::regexp::str_match(s, &arg0(&args)),
7217        "matchAll" => crate::regexp::str_match_all(s, &arg0(&args)),
7218        "search" => {
7219            if is_regexp_arg(&arg0(&args)) {
7220                crate::regexp::str_search(s, &arg0(&args))
7221            } else {
7222                // A string arg is coerced to a (literal) regex; we approximate with
7223                // a plain substring search, which agrees for non-metacharacter
7224                // needles.
7225                let needle = with_host(|h| h.str_of(&arg0(&args)));
7226                Ok(Value::Float(byte_to_unit_index(s, s.find(&needle))))
7227            }
7228        }
7229        "replace" => {
7230            let pat = arg0(&args);
7231            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
7232            if is_regexp_arg(&pat) {
7233                crate::regexp::str_replace_regex(s, &pat, &repl, false)
7234            } else if with_host(|h| host::is_callable(h, &repl)) {
7235                Ok(new_s(replace_str_fn(
7236                    s,
7237                    &with_host(|h| h.str_of(&pat)),
7238                    &repl,
7239                    false,
7240                )?))
7241            } else {
7242                let from = with_host(|h| h.str_of(&pat));
7243                let to = with_host(|h| h.str_of(&repl));
7244                Ok(new_s(s.replacen(&from, &to, 1)))
7245            }
7246        }
7247        "replaceAll" => {
7248            let pat = arg0(&args);
7249            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
7250            if is_regexp_arg(&pat) {
7251                crate::regexp::str_replace_regex(s, &pat, &repl, true)
7252            } else if with_host(|h| host::is_callable(h, &repl)) {
7253                Ok(new_s(replace_str_fn(
7254                    s,
7255                    &with_host(|h| h.str_of(&pat)),
7256                    &repl,
7257                    true,
7258                )?))
7259            } else {
7260                let from = with_host(|h| h.str_of(&pat));
7261                let to = with_host(|h| h.str_of(&repl));
7262                Ok(new_s(s.replace(&from, &to)))
7263            }
7264        }
7265        "split" => {
7266            if is_regexp_arg(&arg0(&args)) {
7267                let limit = args
7268                    .get(1)
7269                    .filter(|v| !matches!(v, Value::Undef))
7270                    .map(|v| with_host(|h| h.to_number(v)) as usize);
7271                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
7272            }
7273            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
7274                vec![new_s(s.to_string())]
7275            } else {
7276                let sep = with_host(|h| h.str_of(&args[0]));
7277                if sep.is_empty() {
7278                    // `split('')` yields one element per code UNIT, so an astral
7279                    // character becomes its two surrogate halves.
7280                    (0..u.len())
7281                        .filter_map(|i| u.unit_str(i))
7282                        .map(new_s)
7283                        .collect()
7284                } else {
7285                    s.split(&sep as &str)
7286                        .map(|p| new_s(p.to_string()))
7287                        .collect()
7288                }
7289            };
7290            // Optional limit: keep at most `limit` substrings.
7291            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
7292                let n = with_host(|h| h.to_number(lim));
7293                if n.is_finite() && n >= 0.0 {
7294                    parts.truncate(n as usize);
7295                }
7296            }
7297            Ok(with_host(|h| h.new_array(parts)))
7298        }
7299        _ => Err(host::type_error(&format!("{name} is not a function"))),
7300    }
7301}
7302
7303fn new_s(s: String) -> Value {
7304    with_host(|h| h.new_str(s))
7305}
7306
7307/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
7308/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
7309fn clamp_pos(n: f64, len: usize) -> usize {
7310    if n.is_nan() || n <= 0.0 {
7311        0
7312    } else if n >= len as f64 {
7313        len
7314    } else {
7315        n.trunc() as usize
7316    }
7317}
7318
7319/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
7320/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
7321/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
7322fn unit_pos(n: f64) -> Option<usize> {
7323    if n.is_nan() {
7324        Some(0)
7325    } else if n < 0.0 || !n.is_finite() {
7326        None
7327    } else {
7328        Some(n.trunc() as usize)
7329    }
7330}
7331
7332/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
7333/// the needle is compared in the same alphabet the haystack is indexed by.
7334fn needle_units(args: &[Value]) -> crate::utf16::Units {
7335    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
7336}
7337
7338/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
7339/// needle matches at `from` itself, as JS specifies.
7340fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
7341    if needle.is_empty() {
7342        return Some(from.min(hay.len()));
7343    }
7344    if needle.len() > hay.len() {
7345        return None;
7346    }
7347    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
7348}
7349
7350/// The highest index `<= upto` at which `needle` occurs in `hay`.
7351fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
7352    if needle.is_empty() {
7353        return Some(upto.min(hay.len()));
7354    }
7355    if needle.len() > hay.len() {
7356        return None;
7357    }
7358    let last = hay.len() - needle.len();
7359    (0..=upto.min(last))
7360        .rev()
7361        .find(|&i| &hay[i..i + needle.len()] == needle)
7362}
7363
7364/// A UTF-8 byte offset reported back to JS as a string position — a UTF-16
7365/// code-unit index — or `-1` for "not found".
7366fn byte_to_unit_index(s: &str, byte: Option<usize>) -> f64 {
7367    match byte {
7368        Some(b) => crate::utf16::index_of_byte(s, b).get() as f64,
7369        None => -1.0,
7370    }
7371}
7372
7373fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
7374    let target_f = arg_num(args, 0);
7375    let target = if target_f.is_finite() && target_f > 0.0 {
7376        target_f as usize
7377    } else {
7378        0
7379    };
7380    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
7381    // is `'-𝒳'` in node, not `'--𝒳'`.
7382    let cur = crate::utf16::len(s);
7383    if cur >= target {
7384        return Ok(s.to_string());
7385    }
7386    let filler = if args.len() >= 2 {
7387        with_host(|h| h.str_of(&args[1]))
7388    } else {
7389        " ".to_string()
7390    };
7391    if filler.is_empty() {
7392        return Ok(s.to_string());
7393    }
7394    // Checked only AFTER the two short-circuits, which is the order V8 uses:
7395    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
7396    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
7397    if target_f > host::MAX_STRING_LENGTH as f64 {
7398        return Err(host::invalid_string_length());
7399    }
7400    let need = target - cur;
7401    let fill = crate::utf16::Units::of(&filler);
7402    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
7403    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
7404    // (see src/utf16.rs).
7405    let units: Vec<u16> = (0..need)
7406        .filter_map(|i| fill.unit(i % fill.len()))
7407        .collect();
7408    let padding = crate::utf16::to_string_lossy(&units);
7409    Ok(if start {
7410        format!("{padding}{s}")
7411    } else {
7412        format!("{s}{padding}")
7413    })
7414}
7415
7416/// V8's radix rejection, shared by `Number.prototype.toString` and
7417/// `BigInt.prototype.toString` — one string, because they are one message and
7418/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
7419/// must be").
7420const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
7421
7422/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
7423fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
7424    match name {
7425        "toString" => {
7426            let radix = match args.first() {
7427                None | Some(Value::Undef) => 10,
7428                Some(_) => {
7429                    let t = arg_num(&args, 0).trunc();
7430                    if !(2.0..=36.0).contains(&t) {
7431                        return Err(host::range_error(RADIX_RANGE));
7432                    }
7433                    t as u32
7434                }
7435            };
7436            Ok(new_s(b.to_str_radix(radix)))
7437        }
7438        // `BigInt.prototype.toLocaleString` groups thousands like the Number
7439        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
7440        // returning the bare digits made it the only numeric type that skipped
7441        // grouping. Same en-US-shaped output as `Number.prototype`; the
7442        // `locales`/`options` arguments are ignored (no ICU here).
7443        "toLocaleString" => {
7444            let digits = b.magnitude().to_string();
7445            let sign = if b.sign() == num_bigint::Sign::Minus {
7446                "-"
7447            } else {
7448                ""
7449            };
7450            Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
7451        }
7452        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
7453        _ => Err(host::type_error(&format!("{name} is not a function"))),
7454    }
7455}
7456
7457fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
7458    match name {
7459        "toFixed" => {
7460            let digits = arg_num(&args, 0);
7461            if !(0.0..=100.0).contains(&digits.trunc()) {
7462                return Err(host::range_error(
7463                    "toFixed() digits argument must be between 0 and 100",
7464                ));
7465            }
7466            Ok(new_s(to_fixed(n, digits as usize)))
7467        }
7468        "toExponential" => {
7469            // `undefined` (or a missing argument) selects the shortest form.
7470            let f = match args.first() {
7471                None | Some(Value::Undef) => None,
7472                Some(_) => {
7473                    let d = arg_num(&args, 0).trunc();
7474                    if !(0.0..=100.0).contains(&d) {
7475                        return Err(host::range_error(
7476                            "toExponential() argument must be between 0 and 100",
7477                        ));
7478                    }
7479                    Some(d as usize)
7480                }
7481            };
7482            Ok(new_s(to_exponential(n, f)))
7483        }
7484        "toString" => {
7485            // An out-of-range radix THROWS; it does not silently fall back to
7486            // base 10. `(1).toString(37)` returned "1" here, so a support probe
7487            // was told every radix worked.
7488            let radix = match args.first() {
7489                None | Some(Value::Undef) => 10,
7490                Some(_) => {
7491                    let r = arg_num(&args, 0);
7492                    let t = r.trunc();
7493                    if !(2.0..=36.0).contains(&t) {
7494                        return Err(host::range_error(RADIX_RANGE));
7495                    }
7496                    t as u32
7497                }
7498            };
7499            if radix == 10 {
7500                Ok(new_s(host::fmt_number(n)))
7501            } else {
7502                Ok(new_s(to_radix(n, radix)))
7503            }
7504        }
7505        "toPrecision" => {
7506            // `undefined` (or a missing argument) behaves like `toString()`.
7507            match args.first() {
7508                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
7509                Some(_) => {
7510                    let p = arg_num(&args, 0).trunc();
7511                    if !(1.0..=100.0).contains(&p) {
7512                        return Err(host::range_error(
7513                            "toPrecision() argument must be between 1 and 100",
7514                        ));
7515                    }
7516                    Ok(new_s(to_precision(n, p as usize)))
7517                }
7518            }
7519        }
7520        "toLocaleString" => Ok(new_s(to_locale_string(n))),
7521        "valueOf" => Ok(Value::Float(n)),
7522        _ => Err(host::type_error(&format!("{name} is not a function"))),
7523    }
7524}
7525
7526/// `Number.prototype.toLocaleString()` with the default locale and options:
7527/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
7528/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
7529/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
7530/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
7531/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
7532fn to_locale_string(n: f64) -> String {
7533    if n.is_nan() {
7534        return "NaN".to_string();
7535    }
7536    if n.is_infinite() {
7537        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
7538    }
7539    let neg = n.is_sign_negative();
7540    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
7541    // (and a bare trailing point). `to_fixed` rounds half away from zero.
7542    // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
7543    // which is exponential — and the grouping below then chopped up the
7544    // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
7545    // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
7546    // source: node groups the shortest decimal form, so `(1e100)
7547    // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
7548    // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
7549    // deliberately different rule — see `bigint_ctor`.)
7550    let fixed = expand_exponential(&to_fixed(n.abs(), 3));
7551    let trimmed = match fixed.split_once('.') {
7552        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
7553        None => fixed.as_str(),
7554    };
7555    let (int_part, frac_part) = match trimmed.split_once('.') {
7556        Some((i, f)) => (i, Some(f)),
7557        None => (trimmed, None),
7558    };
7559    let mut out = String::new();
7560    if neg {
7561        out.push('-'); // Intl keeps the sign even for -0.
7562    }
7563    out.push_str(&group_thousands(int_part));
7564    if let Some(f) = frac_part {
7565        out.push('.');
7566        out.push_str(f);
7567    }
7568    out
7569}
7570
7571/// Write a nonnegative decimal string in plain positional form, expanding an
7572/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
7573/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
7574/// negative exponent (a magnitude below 1, which the caller has already rounded
7575/// to zero), is returned unchanged.
7576fn expand_exponential(s: &str) -> String {
7577    let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
7578        return s.to_string();
7579    };
7580    let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
7581        return s.to_string();
7582    };
7583    if exp <= 0 {
7584        return s.to_string();
7585    }
7586    let (int_digits, frac_digits) = match mantissa.split_once('.') {
7587        Some((i, f)) => (i.to_string(), f.to_string()),
7588        None => (mantissa.to_string(), String::new()),
7589    };
7590    let mut digits = int_digits;
7591    digits.push_str(&frac_digits);
7592    // The exponent consumes the fractional digits first; whatever is left
7593    // becomes trailing zeros.
7594    let zeros = exp as usize - frac_digits.len().min(exp as usize);
7595    digits.push_str(&"0".repeat(zeros));
7596    digits
7597}
7598
7599/// Insert `,` as a thousands separator into a nonnegative integer digit string.
7600fn group_thousands(int_part: &str) -> String {
7601    let bytes = int_part.as_bytes();
7602    let n = bytes.len();
7603    let mut out = String::with_capacity(n + n / 3);
7604    for (i, &b) in bytes.iter().enumerate() {
7605        if i > 0 && (n - i) % 3 == 0 {
7606            out.push(',');
7607        }
7608        out.push(b as char);
7609    }
7610    out
7611}
7612
7613/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
7614/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
7615/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
7616/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
7617///
7618/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
7619/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
7620/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
7621fn to_fixed(n: f64, f: usize) -> String {
7622    if !n.is_finite() {
7623        return host::fmt_number(n);
7624    }
7625    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
7626    if n.abs() >= 1e21 {
7627        return host::fmt_number(n);
7628    }
7629    let neg = n < 0.0;
7630    // Exact decimal with guard digits past the rounding position; then round the
7631    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
7632    let full = format!("{:.*}", f + 25, n.abs());
7633    let mut body = round_decimal_string(&full, f);
7634    if neg {
7635        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
7636    }
7637    body
7638}
7639
7640/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
7641/// fractional digits, half away from zero, propagating carry across the point.
7642fn round_decimal_string(s: &str, f: usize) -> String {
7643    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
7644    let mut digits: Vec<u8> = int_part
7645        .bytes()
7646        .chain(frac_part.bytes())
7647        .map(|b| b - b'0')
7648        .collect();
7649    let point = int_part.len(); // digits before the decimal point
7650    let keep = point + f; // number of leading digits to keep
7651
7652    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
7653    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
7654        let mut i = keep;
7655        loop {
7656            if i == 0 {
7657                digits.insert(0, 1);
7658                // A new leading digit shifts the decimal point right by one.
7659                return assemble_decimal(&digits, point + 1, f);
7660            }
7661            i -= 1;
7662            if digits[i] == 9 {
7663                digits[i] = 0;
7664            } else {
7665                digits[i] += 1;
7666                break;
7667            }
7668        }
7669    }
7670    assemble_decimal(&digits, point, f)
7671}
7672
7673/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
7674/// `point` digits precede the decimal point.
7675fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
7676    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
7677    let int_str = int_str.trim_start_matches('0');
7678    let int_str = if int_str.is_empty() { "0" } else { int_str };
7679    if f == 0 {
7680        return int_str.to_string();
7681    }
7682    let frac: String = digits[point..point + f]
7683        .iter()
7684        .map(|d| (d + b'0') as char)
7685        .collect();
7686    format!("{int_str}.{frac}")
7687}
7688
7689/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
7690/// from zero, returning the `p` digits and the decimal exponent `e` such that the
7691/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
7692/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
7693/// exact digits are taken with guard positions and rounded here.
7694fn round_significant(a: f64, p: usize) -> (String, i32) {
7695    let sci = format!("{a:.*e}", p - 1 + 25);
7696    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
7697    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
7698    let all: Vec<u8> = mant
7699        .chars()
7700        .filter(|c| c.is_ascii_digit())
7701        .map(|c| c as u8 - b'0')
7702        .collect();
7703    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
7704    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
7705        // Round the p-digit mantissa up, propagating carry; a carry out of the
7706        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
7707        let mut d: Vec<u8> = all[..p].to_vec();
7708        let mut i = p;
7709        loop {
7710            if i == 0 {
7711                d.insert(0, 1);
7712                d.truncate(p);
7713                e += 1;
7714                break;
7715            }
7716            i -= 1;
7717            if d[i] == 9 {
7718                d[i] = 0;
7719            } else {
7720                d[i] += 1;
7721                break;
7722            }
7723        }
7724        s = d.iter().map(|x| (x + b'0') as char).collect();
7725    }
7726    (s, e)
7727}
7728
7729/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
7730/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
7731/// `f` omitted, as many digits as uniquely identify the value are used
7732/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
7733/// on the exact value, matching `toPrecision`.
7734fn to_exponential(n: f64, f: Option<usize>) -> String {
7735    if !n.is_finite() {
7736        return host::fmt_number(n);
7737    }
7738    let neg = n < 0.0;
7739    let a = n.abs();
7740    let (s, e) = if a == 0.0 {
7741        // Zero has no significant digits: emit "0" padded to the requested width.
7742        ("0".repeat(f.unwrap_or(0) + 1), 0)
7743    } else {
7744        match f {
7745            Some(f) => round_significant(a, f + 1),
7746            None => {
7747                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
7748                let sci = format!("{a:e}");
7749                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
7750                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
7751                let trimmed = digits.trim_end_matches('0');
7752                let digits = if trimmed.is_empty() { "0" } else { trimmed };
7753                (digits.to_string(), exp_str.parse().unwrap_or(0))
7754            }
7755        }
7756    };
7757    let sign = if e >= 0 { '+' } else { '-' };
7758    let mag = e.abs();
7759    let body = if s.len() == 1 {
7760        format!("{s}e{sign}{mag}")
7761    } else {
7762        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
7763    };
7764    if neg {
7765        format!("-{body}")
7766    } else {
7767        body
7768    }
7769}
7770
7771/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
7772/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
7773/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
7774/// retained (`(100).toPrecision(5) === "100.00"`).
7775fn to_precision(n: f64, p: usize) -> String {
7776    if !n.is_finite() {
7777        return host::fmt_number(n);
7778    }
7779    if n == 0.0 {
7780        return if p == 1 {
7781            "0".into()
7782        } else {
7783            format!("0.{}", "0".repeat(p - 1))
7784        };
7785    }
7786    let neg = n < 0.0;
7787    let (s, e) = round_significant(n.abs(), p);
7788    let pp = p as i32;
7789
7790    let body = if e < -6 || e >= pp {
7791        // Exponential: first digit, optional '.rest', signed exponent.
7792        let sign = if e >= 0 { '+' } else { '-' };
7793        let mag = e.abs();
7794        if p == 1 {
7795            format!("{s}e{sign}{mag}")
7796        } else {
7797            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
7798        }
7799    } else if e >= 0 {
7800        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
7801        let ip = (e + 1) as usize;
7802        if ip == p {
7803            s
7804        } else {
7805            format!("{}.{}", &s[..ip], &s[ip..])
7806        }
7807    } else {
7808        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
7809        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
7810    };
7811    if neg {
7812        format!("-{body}")
7813    } else {
7814        body
7815    }
7816}
7817
7818/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
7819/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
7820/// is emitted exact, and fractional digits are produced up to the input double's
7821/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
7822/// carry-over back into already-written digits (and into the integer part).
7823fn to_radix(n: f64, radix: u32) -> String {
7824    if !n.is_finite() {
7825        return host::fmt_number(n);
7826    }
7827    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
7828    let rf = radix as f64;
7829    let neg = n < 0.0;
7830    let value = n.abs();
7831
7832    let mut integer = value.floor();
7833    let mut fraction = value - integer;
7834
7835    // Fraction digits, most-significant first.
7836    let mut frac: Vec<u8> = Vec::new();
7837    // Only compute fractional digits down to the input double's precision.
7838    let mut delta = 0.5 * (next_up(value) - value);
7839    delta = delta.max(next_up(0.0));
7840    if fraction >= delta {
7841        loop {
7842            // Shift up by one digit.
7843            fraction *= rf;
7844            delta *= rf;
7845            let digit = fraction as usize;
7846            frac.push(digits[digit]);
7847            fraction -= digit as f64;
7848            // Round to even.
7849            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
7850                // Carry-over: back-trace already-written fraction digits.
7851                loop {
7852                    match frac.pop() {
7853                        None => {
7854                            // Carried past the point into the integer part.
7855                            integer += 1.0;
7856                            break;
7857                        }
7858                        Some(c) => {
7859                            let d = if c > b'9' {
7860                                (c - b'a' + 10) as u32
7861                            } else {
7862                                (c - b'0') as u32
7863                            };
7864                            if d + 1 < radix {
7865                                frac.push(digits[(d + 1) as usize]);
7866                                break;
7867                            }
7868                            // digit was radix-1: drop it and keep carrying.
7869                        }
7870                    }
7871                }
7872                break;
7873            }
7874            if fraction < delta {
7875                break;
7876            }
7877        }
7878    }
7879
7880    // Integer digits, least-significant first (reversed at the end).
7881    let mut int_out: Vec<u8> = Vec::new();
7882    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
7883    while v8_exponent(integer / rf) > 0 {
7884        integer /= rf;
7885        int_out.push(b'0');
7886    }
7887    loop {
7888        let remainder = integer % rf;
7889        int_out.push(digits[remainder as usize]);
7890        integer = (integer - remainder) / rf;
7891        if integer <= 0.0 {
7892            break;
7893        }
7894    }
7895    int_out.reverse();
7896
7897    let mut out: Vec<u8> = Vec::new();
7898    if neg {
7899        out.push(b'-');
7900    }
7901    out.extend_from_slice(&int_out);
7902    if !frac.is_empty() {
7903        out.push(b'.');
7904        out.extend_from_slice(&frac);
7905    }
7906    String::from_utf8(out).unwrap()
7907}
7908
7909/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
7910fn next_up(x: f64) -> f64 {
7911    f64::from_bits(x.to_bits() + 1)
7912}
7913
7914/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
7915/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
7916fn v8_exponent(x: f64) -> i32 {
7917    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
7918    if biased == 0 {
7919        -1074 // denormal
7920    } else {
7921        biased - 1075
7922    }
7923}
7924
7925// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
7926
7927/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
7928/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
7929/// but the value kept alongside it is what iteration and `console.log` report,
7930/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
7931fn normalize_zero_key(v: Value) -> Value {
7932    match v {
7933        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
7934        other => other,
7935    }
7936}
7937
7938fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
7939    match name {
7940        "get" => {
7941            let key = with_host(|h| host::map_key(h, &arg0(&args)));
7942            Ok(with_host(|h| match h.get(recv) {
7943                Some(JsObj::Map { entries, .. }) => entries
7944                    .get(&key)
7945                    .map(|(_, v)| v.clone())
7946                    .unwrap_or(Value::Undef),
7947                _ => Value::Undef,
7948            }))
7949        }
7950        "set" => {
7951            let kv = normalize_zero_key(arg0(&args));
7952            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
7953            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
7954            let key = with_host(|h| host::map_key(h, &kv));
7955            with_host(|h| {
7956                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
7957                    entries.insert(key, (kv, vv));
7958                }
7959            });
7960            Ok(recv.clone())
7961        }
7962        "has" => {
7963            let key = with_host(|h| host::map_key(h, &arg0(&args)));
7964            Ok(Value::Bool(with_host(
7965                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
7966            )))
7967        }
7968        "delete" => {
7969            let key = with_host(|h| host::map_key(h, &arg0(&args)));
7970            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
7971                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
7972                _ => false,
7973            })))
7974        }
7975        "clear" => {
7976            with_host(|h| {
7977                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
7978                    entries.clear();
7979                }
7980            });
7981            Ok(Value::Undef)
7982        }
7983        "forEach" => {
7984            let cb = arg0(&args);
7985            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
7986                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
7987                _ => Vec::new(),
7988            });
7989            for (k, v) in pairs {
7990                host::invoke(&cb, vec![v, k, recv.clone()], None)?;
7991            }
7992            Ok(Value::Undef)
7993        }
7994        "keys" | "values" | "entries" | "@@iterator" => {
7995            let items: Vec<Value> = with_host(|h| {
7996                let pairs: Vec<(Value, Value)> = match h.get(recv) {
7997                    Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
7998                    _ => Vec::new(),
7999                };
8000                pairs
8001                    .into_iter()
8002                    .map(|(k, v)| match name {
8003                        "keys" => k,
8004                        "values" => v,
8005                        _ => h.new_array(vec![k, v]), // entries + @@iterator
8006                    })
8007                    .collect()
8008            });
8009            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8010        }
8011        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
8012    }
8013}
8014
8015/// A weak collection can only hold objects (and unregistered symbols) — a
8016/// primitive key is a `TypeError`, which is how packages probe for weak support.
8017fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
8018    let weak = with_host(|h| {
8019        matches!(
8020            h.get(recv),
8021            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
8022        )
8023    });
8024    if !weak {
8025        return Ok(());
8026    }
8027    let is_object = with_host(|h| match key {
8028        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
8029        _ => false,
8030    });
8031    if is_object {
8032        return Ok(());
8033    }
8034    Err(host::type_error(if kind == "WeakMap" {
8035        "Invalid value used as weak map key"
8036    } else {
8037        "Invalid value used in weak set"
8038    }))
8039}
8040
8041fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8042    match name {
8043        "add" => {
8044            let vv = normalize_zero_key(arg0(&args));
8045            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
8046            let key = with_host(|h| host::map_key(h, &vv));
8047            with_host(|h| {
8048                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
8049                    entries.insert(key, vv);
8050                }
8051            });
8052            Ok(recv.clone())
8053        }
8054        "has" => {
8055            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8056            Ok(Value::Bool(with_host(
8057                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
8058            )))
8059        }
8060        "delete" => {
8061            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8062            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
8063                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
8064                _ => false,
8065            })))
8066        }
8067        "clear" => {
8068            with_host(|h| {
8069                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
8070                    entries.clear();
8071                }
8072            });
8073            Ok(Value::Undef)
8074        }
8075        "forEach" => {
8076            let cb = arg0(&args);
8077            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
8078                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
8079                _ => Vec::new(),
8080            });
8081            for v in vals {
8082                host::invoke(&cb, vec![v.clone(), v, recv.clone()], None)?;
8083            }
8084            Ok(Value::Undef)
8085        }
8086        "keys" | "values" | "entries" | "@@iterator" => {
8087            let items: Vec<Value> = with_host(|h| {
8088                let vals: Vec<Value> = match h.get(recv) {
8089                    Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
8090                    _ => Vec::new(),
8091                };
8092                if name == "entries" {
8093                    vals.into_iter()
8094                        .map(|v| h.new_array(vec![v.clone(), v]))
8095                        .collect()
8096                } else {
8097                    vals
8098                }
8099            });
8100            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8101        }
8102        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
8103    }
8104}
8105
8106fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8107    // An `async function*` object's methods return PROMISES of the record, and
8108    // its body has to be driven through the await-aware stepper (a plain
8109    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
8110    if host::is_async_generator(recv) {
8111        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
8112        // `.return`/`.throw` must wait behind a `.next()` that is still
8113        // suspended on an internal `await`, or that `.next()` would report
8114        // `{done: true}` for a value the body had not yet reached. An uncaught
8115        // `.throw(e)` rejects the returned promise; it does not throw here.
8116        return match name {
8117            "next" => Ok(host::async_gen_enqueue(
8118                recv,
8119                host::GenReq::Next(arg0(&args)),
8120            )),
8121            "return" => Ok(host::async_gen_enqueue(
8122                recv,
8123                host::GenReq::Return(arg0(&args)),
8124            )),
8125            "throw" => Ok(host::async_gen_enqueue(
8126                recv,
8127                host::GenReq::Throw(arg0(&args)),
8128            )),
8129            "@@asyncIterator" => Ok(recv.clone()),
8130            _ => Err(host::type_error(&format!(
8131                "asyncGenerator.{name} is not a function"
8132            ))),
8133        };
8134    }
8135    match name {
8136        "next" => {
8137            let send = arg0(&args);
8138            match host::gen_resume(recv, send)? {
8139                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8140                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8141            }
8142        }
8143        "return" => {
8144            // Resume with an injected return so any pending `finally` runs; the
8145            // completion may itself be a `finally` yield (not-done) or the value.
8146            match host::gen_return(recv, arg0(&args))? {
8147                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8148                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8149            }
8150        }
8151        "throw" => {
8152            // Inject a throw at the suspension point: an enclosing `try/catch` in
8153            // the body can handle it (and any `finally` runs); otherwise it
8154            // propagates to the caller.
8155            match host::gen_throw(recv, arg0(&args))? {
8156                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8157                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8158            }
8159        }
8160        _ => Err(host::type_error(&format!(
8161            "generator.{name} is not a function"
8162        ))),
8163    }
8164}
8165
8166/// A `{ value, done }` iterator-result object.
8167fn iter_result(value: Value, done: bool) -> Value {
8168    with_host(|h| {
8169        let mut m: IndexMap<String, Value> = IndexMap::new();
8170        m.insert("value".into(), value);
8171        m.insert("done".into(), Value::Bool(done));
8172        h.new_object(m)
8173    })
8174}
8175
8176/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
8177/// cursor over a materialized item list.
8178fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8179    match name {
8180        "next" => {
8181            let step = with_host(|h| {
8182                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
8183                    if *idx < items.len() {
8184                        let v = items[*idx].clone();
8185                        *idx += 1;
8186                        return Some(v);
8187                    }
8188                }
8189                None
8190            });
8191            Ok(match step {
8192                Some(v) => iter_result(v, false),
8193                None => iter_result(Value::Undef, true),
8194            })
8195        }
8196        "return" => {
8197            // Exhaust the cursor and report done.
8198            with_host(|h| {
8199                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
8200                    *idx = items.len();
8201                }
8202            });
8203            Ok(iter_result(arg0(&args), true))
8204        }
8205        // An iterator is its own iterable.
8206        "@@iterator" => Ok(recv.clone()),
8207        _ => Err(host::type_error(&format!(
8208            "iterator.{name} is not a function"
8209        ))),
8210    }
8211}
8212
8213fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
8214    match name {
8215        "toString" => Ok(with_host(|h| {
8216            let s = h.str_of(recv);
8217            h.new_str(s)
8218        })),
8219        _ => Err(host::type_error(&format!(
8220            "symbol.{name} is not a function"
8221        ))),
8222    }
8223}
8224
8225// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
8226
8227fn object_create(args: Vec<Value>) -> Result<Value, String> {
8228    let proto = arg0(&args);
8229    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
8230    // `undefined` is NOT accepted — measured on node v26.7.0,
8231    // `Object.create(undefined)` is
8232    // `TypeError: Object prototype may only be an Object or null: undefined`,
8233    // where node-js quietly built a normal object.
8234    reject_bad_prototype(&proto)?;
8235    let obj = with_host(|h| h.new_object(IndexMap::new()));
8236    // `set_proto` records a null proto as an explicit null-prototype object.
8237    with_host(|h| h.set_proto(&obj, proto));
8238    // Optional second arg: a property-descriptor map.
8239    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
8240        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
8241            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
8242            _ => Vec::new(),
8243        });
8244        for (k, d) in entries {
8245            apply_descriptor(&obj, &k, &d);
8246        }
8247    }
8248    Ok(obj)
8249}
8250
8251/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
8252/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
8253/// `EventEmitter.prototype` (the one express mixes onto its app function).
8254fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
8255    match ns {
8256        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
8257        _ => None,
8258    }
8259}
8260
8261/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
8262/// from its `ownKeys` trap (the symbol half of the same list the string keys are
8263/// filtered out of); every other receiver answers from its property map.
8264fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
8265    if let Some(keys) = crate::proxy::own_keys(v)? {
8266        return Ok(keys
8267            .iter()
8268            .filter(|k| host::is_symbol_key(k))
8269            .map(|k| crate::proxy::key_value(k))
8270            .collect());
8271    }
8272    Ok(with_host(|h| h.own_symbol_keys(v)))
8273}
8274
8275/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
8276pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
8277    object_define_property(vec![obj.clone(), key, desc])
8278}
8279
8280/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
8281pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
8282    object_get_own_descriptor(vec![obj.clone(), key])
8283}
8284
8285fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
8286    let obj = arg0(&args);
8287    // A Proxy defines through its `defineProperty` trap; the target it forwards
8288    // to is where the ordinary path below finally runs.
8289    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
8290        let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8291        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
8292        if !with_host(|h| is_object_like(h, &desc)) {
8293            return Err(host::type_error(&format!(
8294                "Property description must be an object: {}",
8295                with_host(|h| h.str_of(&desc))
8296            )));
8297        }
8298        crate::proxy::define_property(&obj, &key, &desc)?;
8299        return Ok(obj);
8300    }
8301    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
8302    // target and a non-object descriptor each throw before anything is written.
8303    if !with_host(|h| is_object_like(h, &obj)) {
8304        return Err(host::type_error(
8305            "Object.defineProperty called on non-object",
8306        ));
8307    }
8308    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
8309    if !with_host(|h| is_object_like(h, &desc)) {
8310        return Err(host::type_error(&format!(
8311            "Property description must be an object: {}",
8312            with_host(|h| h.str_of(&desc))
8313        )));
8314    }
8315    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8316    apply_descriptor(&obj, &key, &desc);
8317    Ok(obj)
8318}
8319
8320/// Whether `v` is an Object in the language sense — anything `typeof` calls
8321/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
8322/// distinguish "an object" from a primitive.
8323fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
8324    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
8325}
8326
8327/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
8328/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
8329/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
8330/// `assign` family: each returned an empty result for `null` where node v26.7.0
8331/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
8332/// is coercible and keeps working (`Object.keys(1)` is `[]`).
8333fn require_object_coercible(v: &Value) -> Result<(), String> {
8334    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
8335        return Err(host::type_error(
8336            "Cannot convert undefined or null to object",
8337        ));
8338    }
8339    Ok(())
8340}
8341
8342/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
8343/// nor `null`, with V8's wording. Measured on node v26.7.0:
8344/// `Object.create("s")` is
8345/// `TypeError: Object prototype may only be an Object or null: s`.
8346fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
8347    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
8348        return Ok(());
8349    }
8350    Err(host::type_error(&format!(
8351        "Object prototype may only be an Object or null: {}",
8352        with_host(|h| h.str_of(proto))
8353    )))
8354}
8355
8356/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
8357///
8358/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
8359/// `configurable` field defaults to **false** — which is why a `defineProperty`
8360/// data property is invisible to `Object.keys` unless the caller opts in. That
8361/// asymmetry against plain assignment is the whole reason the attribute table
8362/// exists.
8363fn apply_descriptor(obj: &Value, key: &str, desc: &Value) {
8364    let (value, get, set, attrs) = with_host(|h| match h.get(desc) {
8365        Some(JsObj::Object(p)) => {
8366            let flag = |n: &str| p.get(n).map(|v| h.truthy(v)).unwrap_or(false);
8367            (
8368                p.get("value").cloned(),
8369                p.get("get").cloned(),
8370                p.get("set").cloned(),
8371                host::PropAttrs {
8372                    writable: flag("writable"),
8373                    enumerable: flag("enumerable"),
8374                    configurable: flag("configurable"),
8375                },
8376            )
8377        }
8378        _ => (None, None, None, host::PropAttrs::default()),
8379    });
8380    with_host(|h| h.set_prop_attrs(obj, key, attrs));
8381    if get.is_some() || set.is_some() {
8382        with_host(|h| h.set_accessor(obj, key, get, set));
8383    } else if let Some(v) = value {
8384        // A function/class receiver stores its own props in the fn-prop side table
8385        // (express `mixin(app, proto)` defines methods onto the `app` *function*).
8386        if matches!(
8387            with_host(|h| h.get(obj).cloned()),
8388            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
8389        ) {
8390            with_host(|h| h.set_fn_prop(obj, key, v));
8391        } else if let (Some(ObjKind::Array), Ok(i)) =
8392            (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
8393        {
8394            // An array's index keys ARE its elements, and defining one past the
8395            // end grows the array with holes in between (10.4.2.1). This whole
8396            // branch used to be missing: `Object.defineProperty(arr, 1, {value})`
8397            // wrote into the ordinary property map an array does not have, so it
8398            // was a silent no-op.
8399            with_host(|h| {
8400                let old = match h.get(obj) {
8401                    Some(JsObj::Array(items)) => items.len(),
8402                    _ => 0,
8403                };
8404                if let Some(JsObj::Array(items)) = h.get_mut(obj) {
8405                    if i >= old {
8406                        items.resize(i + 1, Value::Undef);
8407                    }
8408                    items[i] = v;
8409                }
8410                if i > old {
8411                    h.mark_hole_range(obj, old..i);
8412                }
8413                h.clear_hole(obj, i);
8414            });
8415        } else {
8416            with_host(|h| {
8417                if let Some(JsObj::Object(p)) = h.get_mut(obj) {
8418                    p.insert(key.to_string(), v);
8419                    host::canonicalize_own_keys(p);
8420                }
8421            });
8422        }
8423    }
8424}
8425
8426/// `Object.defineProperties(obj, descriptorMap)`.
8427fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
8428    let obj = arg0(&args);
8429    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
8430    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
8431        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
8432        _ => Vec::new(),
8433    });
8434    for (k, d) in entries {
8435        apply_descriptor(&obj, &k, &d);
8436    }
8437    Ok(obj)
8438}
8439
8440fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
8441    let obj = arg0(&args);
8442    require_object_coercible(&obj)?;
8443    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8444    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
8445        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
8446    }
8447    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
8448    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
8449    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
8450        if let Some(names) = builtin_proto_method_names(&ns) {
8451            if names.contains(&key.as_str()) {
8452                return Ok(with_host(|h| {
8453                    let thunk = h.alloc(JsObj::Builtin(format!(
8454                        "@proto:{}:{key}",
8455                        ns.trim_end_matches(".prototype")
8456                    )));
8457                    let mut m: IndexMap<String, Value> = IndexMap::new();
8458                    m.insert("value".into(), thunk);
8459                    m.insert("writable".into(), Value::Bool(true));
8460                    m.insert("enumerable".into(), Value::Bool(true));
8461                    m.insert("configurable".into(), Value::Bool(true));
8462                    h.new_object(m)
8463                }));
8464            }
8465        }
8466    }
8467    // Accessor descriptor?
8468    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
8469        return Ok(with_host(|h| {
8470            let a = h.prop_attrs(&obj, &key);
8471            let mut m: IndexMap<String, Value> = IndexMap::new();
8472            m.insert("get".into(), get.unwrap_or(Value::Undef));
8473            m.insert("set".into(), set.unwrap_or(Value::Undef));
8474            m.insert("enumerable".into(), Value::Bool(a.enumerable));
8475            m.insert("configurable".into(), Value::Bool(a.configurable));
8476            h.new_object(m)
8477        }));
8478    }
8479    let val = with_host(|h| match h.get(&obj) {
8480        // A Buffer's own properties are exactly its byte indices, read out of the
8481        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
8482        // that V8 keeps on the prototype, so they own no descriptor.
8483        Some(JsObj::Object(p))
8484            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
8485        {
8486            match (
8487                p.get("@@bytes").and_then(|b| h.get(b)),
8488                key.parse::<usize>(),
8489            ) {
8490                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
8491                _ => None,
8492            }
8493        }
8494        Some(JsObj::Object(p)) => p.get(&key).cloned(),
8495        // An array's index keys read the elements; `length` is the exotic own
8496        // property; anything else is an ordinary own key in the side table.
8497        Some(JsObj::Array(items)) => match key.parse::<usize>() {
8498            // An ELIDED index owns no property at all, so it has no descriptor.
8499            Ok(i) if h.is_hole(&obj, i) => None,
8500            Ok(i) => items.get(i).cloned(),
8501            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
8502            Err(_) => h.fn_prop(&obj, &key),
8503        },
8504        // A function/class own prop lives in the fn-prop side table.
8505        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
8506        _ => None,
8507    });
8508    match val {
8509        Some(v) => Ok(with_host(|h| {
8510            let a = h.prop_attrs(&obj, &key);
8511            let mut m: IndexMap<String, Value> = IndexMap::new();
8512            m.insert("value".into(), v);
8513            m.insert("writable".into(), Value::Bool(a.writable));
8514            m.insert("enumerable".into(), Value::Bool(a.enumerable));
8515            m.insert("configurable".into(), Value::Bool(a.configurable));
8516            h.new_object(m)
8517        })),
8518        None => Ok(Value::Undef),
8519    }
8520}
8521
8522/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
8523/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
8524/// the standard "clone with accessors intact" idiom, so this must agree
8525/// key-for-key with `getOwnPropertyNames`.
8526fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
8527    let obj = arg0(&args);
8528    let names = object_keys(vec![obj.clone()], 3)?;
8529    let keys: Vec<String> = with_host(|h| match h.get(&names) {
8530        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
8531        _ => Vec::new(),
8532    });
8533    let mut out: IndexMap<String, Value> = IndexMap::new();
8534    for k in keys {
8535        let ks = with_host(|h| h.new_str(k.clone()));
8536        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
8537        if !matches!(d, Value::Undef) {
8538            out.insert(k, d);
8539        }
8540    }
8541    Ok(with_host(|h| h.new_object(out)))
8542}
8543
8544/// `key in obj` respecting the prototype chain. Reports a `Result` because a
8545/// Proxy's `has` trap is user code and may throw.
8546pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
8547    if let Some(b) = crate::proxy::has(obj, key)? {
8548        return Ok(b);
8549    }
8550    Ok(has_property_ordinary(obj, key))
8551}
8552
8553/// `[[HasProperty]]` for every non-Proxy receiver.
8554fn has_property_ordinary(obj: &Value, key: &str) -> bool {
8555    // `key in <builtin namespace/prototype>`: membership matches what a property
8556    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
8557    // prototype methods) resolve as callable thunks via `namespace_property`, so
8558    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
8559    // with the `in` operator before reading the intrinsic).
8560    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
8561        return !matches!(namespace_property(&ns, key), Value::Undef);
8562    }
8563    // An integer index of a typed array / Buffer is an own property, and lives
8564    // in the hidden element array rather than the property map — the same
8565    // question `hasOwnProperty` answers, through the same helper. Only a hit
8566    // short-circuits: a non-index key like `'length'` must still fall through
8567    // to the ordinary chain lookup below.
8568    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
8569        return true;
8570    }
8571    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
8572        return true;
8573    }
8574    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
8575        return true;
8576    }
8577    with_host(|h| match h.get(obj) {
8578        Some(JsObj::Object(p)) => p.contains_key(key),
8579        Some(JsObj::Array(items)) => {
8580            key == "length"
8581                || key
8582                    .parse::<usize>()
8583                    .map(|i| i < items.len() && !h.is_hole(obj, i))
8584                    .unwrap_or(false)
8585                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
8586                // side table, and `in` must see it.
8587                || h.fn_prop(obj, key).is_some()
8588        }
8589        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
8590        _ => false,
8591    })
8592}
8593
8594/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
8595/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
8596/// that preserves the *reference graph*. Two properties pointing at the same
8597/// object clone to two properties pointing at the same clone, and a cycle clones
8598/// to a cycle instead of recursing forever. `seen` maps each source heap index
8599/// to its clone, which is what buys both.
8600pub(crate) fn deep_clone(v: &Value) -> Value {
8601    deep_clone_seen(v, &mut std::collections::HashMap::new())
8602}
8603
8604fn deep_clone_seen(v: &Value, seen: &mut std::collections::HashMap<u32, Value>) -> Value {
8605    let idx = match v {
8606        Value::Obj(i) => *i,
8607        _ => return v.clone(),
8608    };
8609    if let Some(done) = seen.get(&idx) {
8610        return done.clone();
8611    }
8612    match with_host(|h| h.get(v).cloned()) {
8613        Some(JsObj::Array(items)) => {
8614            // Register the (empty) clone BEFORE recursing so a self-reference
8615            // resolves to it.
8616            let out = with_host(|h| h.new_array(Vec::new()));
8617            seen.insert(idx, out.clone());
8618            let cloned: Vec<Value> = items.iter().map(|x| deep_clone_seen(x, seen)).collect();
8619            with_host(|h| {
8620                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
8621                    *a = cloned;
8622                }
8623                // A sparse source clones to an equally sparse array: the clone
8624                // walks own properties, so a hole is nothing to copy.
8625                h.copy_holes(v, &out, Some);
8626            });
8627            out
8628        }
8629        Some(JsObj::Object(props)) => {
8630            let out = with_host(|h| h.new_object(IndexMap::new()));
8631            seen.insert(idx, out.clone());
8632            let cloned: IndexMap<String, Value> = props
8633                .iter()
8634                .map(|(k, val)| (k.clone(), deep_clone_seen(val, seen)))
8635                .collect();
8636            with_host(|h| {
8637                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
8638                    *p = cloned;
8639                }
8640                // A native exotic (Buffer, typed array, …) keeps its prototype so
8641                // the clone passes the same brand checks as the source.
8642                if let Some(p) = h.proto_of(v) {
8643                    h.set_proto(&out, p);
8644                }
8645                h.copy_prop_attrs(v, &out);
8646            });
8647            out
8648        }
8649        // Map/Set are structured types: clone the entries, keep the kind.
8650        Some(JsObj::Map { entries, weak }) => {
8651            let out = with_host(|h| {
8652                h.alloc(JsObj::Map {
8653                    entries: IndexMap::new(),
8654                    weak,
8655                })
8656            });
8657            seen.insert(idx, out.clone());
8658            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
8659            for (k, val) in pairs {
8660                let ck = deep_clone_seen(&k, seen);
8661                let cv = deep_clone_seen(&val, seen);
8662                let _ = map_method(&out, "set", vec![ck, cv]);
8663            }
8664            out
8665        }
8666        Some(JsObj::Set { entries, weak }) => {
8667            let out = with_host(|h| {
8668                h.alloc(JsObj::Set {
8669                    entries: IndexMap::new(),
8670                    weak,
8671                })
8672            });
8673            seen.insert(idx, out.clone());
8674            let vals: Vec<Value> = entries.values().cloned().collect();
8675            for x in vals {
8676                let cx = deep_clone_seen(&x, seen);
8677                let _ = set_method(&out, "add", vec![cx]);
8678            }
8679            out
8680        }
8681        // Strings/BigInts/RegExps/dates are immutable-enough to share, and a
8682        // function is not cloneable at all (Node throws DataCloneError; node-js
8683        // passes it through rather than inventing that error class).
8684        _ => v.clone(),
8685    }
8686}
8687
8688// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
8689
8690/// A short `Name: message` string for an error value (used when an await
8691/// rejection unwinds as a thrown error).
8692pub fn error_string(h: &host::JsHost, v: &Value) -> String {
8693    if let Some(JsObj::Object(props)) = h.get(v) {
8694        let name = props
8695            .get("name")
8696            .map(|x| h.str_of(x))
8697            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
8698            .unwrap_or_else(|| "Error".into());
8699        if let Some(m) = props.get("message") {
8700            return format!("{name}: {}", h.str_of(m));
8701        }
8702        return name;
8703    }
8704    h.str_of(v)
8705}
8706
8707fn make_builtin(name: String) -> Value {
8708    with_host(|h| h.alloc(JsObj::Builtin(name)))
8709}
8710
8711/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
8712/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
8713///
8714/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
8715/// an EXPLICIT link, so an object on the default prototype reported `null`:
8716/// `({}).__proto__ === Object.prototype` was false while
8717/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
8718/// the three cannot drift apart again.
8719pub fn prototype_of(v: &Value) -> Value {
8720    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
8721    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
8722    // not `Function.prototype`. This is the class-side half of the subclass
8723    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
8724    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
8725        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
8726    }
8727    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
8728    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
8729    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
8730    // `ClassVal.parent`, but the link itself was invisible, so
8731    // `Object.getPrototypeOf(B) === A` read false and any library walking the
8732    // constructor chain — rather than calling a static — saw a base class.
8733    // A base class keeps the default answer below (`Function.prototype`).
8734    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
8735        if let Some(parent) = c.parent {
8736            return parent;
8737        }
8738    }
8739    // `Object.create(null)` and friends really do have a null prototype.
8740    if with_host(|h| h.has_null_proto(v)) {
8741        return with_host(|h| h.null());
8742    }
8743    if let Some(p) = with_host(|h| h.proto_of(v)) {
8744        return p;
8745    }
8746    // A builtin exotic with no explicit `[[Prototype]]` link reports its
8747    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
8748    // Array.prototype`), which `strict_eq` compares by name. A plain object
8749    // reports the one real `Object.prototype` object.
8750    with_host(|h| {
8751        h.ensure_native_protos();
8752        match default_ctor_name(h, v) {
8753            Some("Object") => h.object_proto(),
8754            Some(c) => h.alloc(JsObj::Builtin(format!("{c}.prototype"))),
8755            None => h.null(),
8756        }
8757    })
8758}
8759
8760/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
8761/// internal resolve/reject functions.
8762fn new_promise(executor: Value) -> Result<Value, String> {
8763    let p = with_host(|h| h.new_promise());
8764    let id = with_host(|h| h.promise_id(&p).unwrap());
8765    let res = make_builtin(format!("@@presolve:{id}"));
8766    let rej = make_builtin(format!("@@preject:{id}"));
8767    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
8768        // A throw in the executor rejects the promise.
8769        let ev = host::take_exc_or_error(&e);
8770        host::reject_promise_val(id, ev);
8771    }
8772    Ok(p)
8773}
8774
8775fn promise_resolve(v: Value) -> Result<Value, String> {
8776    Ok(host::promise_of(&v))
8777}
8778fn promise_reject(v: Value) -> Result<Value, String> {
8779    let p = with_host(|h| h.new_promise());
8780    let id = with_host(|h| h.promise_id(&p).unwrap());
8781    host::reject_promise_val(id, v);
8782    Ok(p)
8783}
8784
8785/// `Promise.withResolvers()` — a fresh pending promise paired with its own
8786/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
8787/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
8788fn promise_with_resolvers() -> Result<Value, String> {
8789    let p = with_host(|h| h.new_promise());
8790    let id = with_host(|h| h.promise_id(&p).unwrap());
8791    let resolve = make_builtin(format!("@@presolve:{id}"));
8792    let reject = make_builtin(format!("@@preject:{id}"));
8793    let mut props: IndexMap<String, Value> = IndexMap::new();
8794    props.insert("promise".into(), p);
8795    props.insert("resolve".into(), resolve);
8796    props.insert("reject".into(), reject);
8797    Ok(with_host(|h| h.new_object(props)))
8798}
8799
8800#[derive(Clone, Copy)]
8801enum AllMode {
8802    All,
8803    AllSettled,
8804}
8805
8806/// `Promise.all` / `Promise.allSettled`.
8807fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
8808    let items = host::iter_all(&arg0(&args))?;
8809    let result = with_host(|h| h.new_promise());
8810    let rid = with_host(|h| h.promise_id(&result).unwrap());
8811    let n = items.len();
8812    if n == 0 {
8813        let empty = with_host(|h| h.new_array(Vec::new()));
8814        host::resolve_promise_val(rid, empty);
8815        return Ok(result);
8816    }
8817    // Shared mutable accumulator via Rc<RefCell<…>>.
8818    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
8819    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
8820    for (i, it) in items.into_iter().enumerate() {
8821        let ap = host::promise_of(&it);
8822        let aid = with_host(|h| h.promise_id(&ap).unwrap());
8823        let slots = slots.clone();
8824        let remaining = remaining.clone();
8825        host::subscribe_native(
8826            aid,
8827            Box::new(move |state, val| {
8828                let settled = match mode {
8829                    AllMode::All => {
8830                        if state == host::PromiseState::Rejected {
8831                            host::reject_promise_val(rid, val);
8832                            return Ok(());
8833                        }
8834                        val
8835                    }
8836                    AllMode::AllSettled => with_host(|h| {
8837                        let mut m: IndexMap<String, Value> = IndexMap::new();
8838                        if state == host::PromiseState::Rejected {
8839                            m.insert("status".into(), h.new_str("rejected"));
8840                            m.insert("reason".into(), val);
8841                        } else {
8842                            m.insert("status".into(), h.new_str("fulfilled"));
8843                            m.insert("value".into(), val);
8844                        }
8845                        h.new_object(m)
8846                    }),
8847                };
8848                slots.borrow_mut()[i] = settled;
8849                let mut r = remaining.borrow_mut();
8850                *r -= 1;
8851                if *r == 0 {
8852                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
8853                    host::resolve_promise_val(rid, arr);
8854                }
8855                Ok(())
8856            }),
8857        );
8858    }
8859    Ok(result)
8860}
8861
8862/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
8863fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
8864    let items = host::iter_all(&arg0(&args))?;
8865    let result = with_host(|h| h.new_promise());
8866    let rid = with_host(|h| h.promise_id(&result).unwrap());
8867    let n = items.len();
8868    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
8869    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
8870    for (i, it) in items.into_iter().enumerate() {
8871        let ap = host::promise_of(&it);
8872        let aid = with_host(|h| h.promise_id(&ap).unwrap());
8873        let errors = errors.clone();
8874        let remaining = remaining.clone();
8875        host::subscribe_native(
8876            aid,
8877            Box::new(move |state, val| {
8878                if any {
8879                    if state == host::PromiseState::Fulfilled {
8880                        host::resolve_promise_val(rid, val);
8881                    } else {
8882                        errors.borrow_mut()[i] = val;
8883                        let mut r = remaining.borrow_mut();
8884                        *r -= 1;
8885                        if *r == 0 {
8886                            // All rejected → AggregateError carrying every reason.
8887                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
8888                            let msg = with_host(|h| h.new_str("All promises were rejected"));
8889                            let agg = make_error("AggregateError", &[reasons, msg]);
8890                            host::reject_promise_val(rid, agg);
8891                        }
8892                    }
8893                } else if state == host::PromiseState::Rejected {
8894                    host::reject_promise_val(rid, val);
8895                } else {
8896                    host::resolve_promise_val(rid, val);
8897                }
8898                Ok(())
8899            }),
8900        );
8901    }
8902    Ok(result)
8903}
8904
8905/// `.then` / `.catch` / `.finally` on a promise.
8906fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8907    match name {
8908        "then" => Ok(host::promise_then(
8909            recv,
8910            args.first().cloned().unwrap_or(Value::Undef),
8911            args.get(1).cloned().unwrap_or(Value::Undef),
8912        )),
8913        "catch" => Ok(host::promise_then(
8914            recv,
8915            Value::Undef,
8916            args.first().cloned().unwrap_or(Value::Undef),
8917        )),
8918        "finally" => {
8919            let cb = arg0(&args);
8920            let i = match cb {
8921                Value::Obj(i) => i,
8922                _ => 0,
8923            };
8924            let pass = make_builtin(format!("@@finpass:{i}"));
8925            let throw = make_builtin(format!("@@finthrow:{i}"));
8926            Ok(host::promise_then(recv, pass, throw))
8927        }
8928        _ => Err(host::type_error(&format!(
8929            "promise.{name} is not a function"
8930        ))),
8931    }
8932}
8933
8934fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
8935    with_host(|h| {
8936        if next_tick {
8937            h.queue_nexttick(cb, args);
8938        } else {
8939            h.queue_micro(cb, args);
8940        }
8941    });
8942}
8943
8944/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
8945/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
8946/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
8947///
8948/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
8949/// fires, so it runs until cleared and — being referenced — holds the process
8950/// open exactly as in Node.
8951fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
8952    let cb = arg0(&args);
8953    let delay = if name == "setImmediate" {
8954        -1.0 // before any 0ms timeout
8955    } else {
8956        args.get(1)
8957            .map(|d| with_host(|h| h.to_number(d)))
8958            .unwrap_or(0.0)
8959            .max(0.0)
8960    };
8961    let extra = if name == "setImmediate" {
8962        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
8963    } else {
8964        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
8965    };
8966    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
8967    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
8968    let interval = (name == "setInterval").then(|| delay.max(1.0));
8969    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
8970    let tag = if name == "setImmediate" {
8971        "Immediate"
8972    } else {
8973        "Timeout"
8974    };
8975    crate::stdlib::timers::new_handle(id, tag)
8976}
8977
8978/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
8979/// by the bare id it coerces to (code that stored `+timer` still works).
8980fn clear_timer(v: &Value) {
8981    let id =
8982        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
8983    with_host(|h| h.cancel_timer(id));
8984}