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::HOIST_VAR, b_hoist_var);
85    vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
86}
87
88/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
89/// runs its pending `finally`; a user iterator object gets its `.return()` called
90/// if present; a plain materialized iterator just drops. Returns `undefined`.
91/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
92/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
93/// is neither is left alone.
94pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
95    if with_host(|h| h.is_generator_val(it)) {
96        host::gen_return(it, Value::Undef)?;
97        return Ok(());
98    }
99    if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
100        if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
101            if with_host(|h| host::is_callable(h, &f)) {
102                host::invoke(&f, Vec::new(), Some(it.clone()))?;
103            }
104        }
105    }
106    Ok(())
107}
108
109fn b_iter_close(vm: &mut VM, _: u8) -> Value {
110    let it = vm.pop();
111    // A `finally` may print or yield, but the loop is done either way; an error
112    // it raises still propagates.
113    match close_iterator(&it) {
114        Ok(()) => Value::Undef,
115        Err(e) => abort(vm, e),
116    }
117}
118
119/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
120/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
121/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
122/// Number nor throws the mix error.
123fn b_num_step(vm: &mut VM, _: u8) -> Value {
124    let old = vm.pop();
125    let tag = match vm.pop() {
126        Value::Int(n) => n,
127        Value::Float(f) => f as i64,
128        _ => 1,
129    };
130    if with_host(|h| h.is_bigint_val(&old)) {
131        let b = with_host(|h| h.as_bigint(&old)).unwrap();
132        let old_n = with_host(|h| h.new_bigint(b.clone()));
133        let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
134        vm.push(old_n);
135        new
136    } else {
137        let n = with_host(|h| h.to_number(&old));
138        vm.push(Value::Float(n));
139        Value::Float(n + tag as f64)
140    }
141}
142
143/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
144/// `{value, done}` record (see `host::async_step`).
145fn b_async_step(vm: &mut VM, _: u8) -> Value {
146    let iter = vm.pop();
147    let r = host::async_step(&iter);
148    finish(vm, r)
149}
150
151/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
152/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
153fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
154    let digits = sval(&vm.pop());
155    match digits.parse::<num_bigint::BigInt>() {
156        Ok(b) => with_host(|h| h.new_bigint(b)),
157        Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
158    }
159}
160
161/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
162/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
163/// Builds the `strings` array (carrying its `.raw` array) and calls
164/// `tag(strings, ...values)`.
165fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
166    let mut all = pop_n(vm, argc as usize);
167    let int_of = |v: &Value| match v {
168        Value::Int(n) => *n as usize,
169        Value::Float(f) => *f as usize,
170        _ => 0,
171    };
172    let tag = all.remove(0);
173    let n = int_of(&all.remove(0));
174    let mcount = int_of(&all.remove(0));
175    let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
176    let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
177    let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
178    // strings = cooked array; strings.raw = raw array (frozen in JS; nothing here
179    // mutates it).
180    let strings = with_host(|h| h.new_array(cooked));
181    let raw_arr = with_host(|h| h.new_array(raw));
182    // `GetTemplateObject` (13.2.8.4) defines `raw` as an own property that is
183    // neither writable, enumerable, nor configurable, then integrity-seals the
184    // template object. So `raw` stays out of `Object.keys(strings)` while
185    // `getOwnPropertyNames` still reports it.
186    with_host(|h| {
187        h.set_fn_prop(&strings, "raw", raw_arr);
188        h.set_prop_attrs(
189            &strings,
190            "raw",
191            host::PropAttrs {
192                writable: false,
193                enumerable: false,
194                configurable: false,
195            },
196        );
197    });
198    let mut call_args = vec![strings];
199    call_args.extend(values);
200    let r = host::invoke(&tag, call_args, None);
201    finish(vm, r)
202}
203
204/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
205/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
206/// iterator (each yielded value is awaited). Returns the iterator object/handle.
207fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
208    let src = vm.pop();
209    let r = host::get_async_iterator(&src);
210    finish(vm, r)
211}
212
213/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
214/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
215/// express (backreference/lookaround) throws a `SyntaxError` here.
216fn b_mkregex(vm: &mut VM, _: u8) -> Value {
217    let flags = sval(&vm.pop());
218    let pattern = sval(&vm.pop());
219    match crate::regexp::build_regexp(&pattern, &flags) {
220        Ok(v) => v,
221        Err(e) => abort(vm, e),
222    }
223}
224
225/// DAP per-statement marker (`node --dap` only; the compiler emits this before
226/// each statement under `debug`). Pops the source line pushed by the preceding
227/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
228/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
229/// session is active.
230fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
231    let line = match vm.pop() {
232        Value::Int(n) => n as u32,
233        _ => 0,
234    };
235    crate::dap::on_debug_line(line);
236    Value::Undef
237}
238
239/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
240/// or `member::SET`). Keeps the object on the stack.
241fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
242    let func = vm.pop();
243    let kind = match vm.pop() {
244        Value::Int(n) => n,
245        _ => 0,
246    };
247    let name = sval(&vm.pop());
248    let obj = vm.pop();
249    with_host(|h| {
250        if kind == host::member::SET {
251            h.set_accessor(&obj, &name, None, Some(func));
252        } else {
253            h.set_accessor(&obj, &name, Some(func), None);
254        }
255    });
256    obj
257}
258
259fn b_await(vm: &mut VM, _: u8) -> Value {
260    let v = vm.pop();
261    match host::await_value(v) {
262        Ok(r) => r,
263        Err(e) => abort(vm, e),
264    }
265}
266
267// ── classes / super / generators / property keys (compiler-emitted ops) ──────
268
269fn b_mkclass(vm: &mut VM, _: u8) -> Value {
270    let ctor = vm.pop();
271    let parent = vm.pop();
272    let name = sval(&vm.pop());
273    host::build_class(&name, parent, ctor)
274}
275
276fn b_def_member(vm: &mut VM, _: u8) -> Value {
277    let func = vm.pop();
278    let is_static = matches!(vm.pop(), Value::Bool(true));
279    let kind = match vm.pop() {
280        Value::Int(n) => n,
281        _ => 0,
282    };
283    let name = sval(&vm.pop());
284    let class_val = vm.pop();
285    host::define_member(&class_val, &name, kind, is_static, func);
286    class_val
287}
288
289fn b_def_field(vm: &mut VM, _: u8) -> Value {
290    // `name_anon`: the initializer was an anonymous function definition, so
291    // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
292    // decided by the compiler, not re-derived from the produced value.
293    let name_anon = matches!(vm.pop(), Value::Bool(true));
294    let thunk = vm.pop();
295    let name = sval(&vm.pop());
296    let class_val = vm.pop();
297    host::define_field(&class_val, &name, thunk, name_anon);
298    class_val
299}
300
301/// `super(...args)` in a derived constructor: run the parent constructor on the
302/// current `this`, then this class's field initializers.
303fn b_super_call(vm: &mut VM, argc: u8) -> Value {
304    let args = pop_n(vm, argc as usize);
305    let this = with_host(|h| h.current_this());
306    let this = match this {
307        Some(t) => t,
308        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
309    };
310    // The class whose constructor is running = the running method's home class.
311    let (parent, fields) = with_host(|h| h.super_context());
312    let (parent, fields) = match parent {
313        Some(p) => (p, fields),
314        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
315    };
316    let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
317    let r = host::super_construct(&parent, args, &this, &nt);
318    if let Err(e) = r {
319        return abort(vm, e);
320    }
321    // Run this (derived) class's own instance-field initializers after super.
322    for (name, thunk, name_anon) in fields {
323        if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
324            return abort(vm, e);
325        }
326    }
327    Value::Undef
328}
329
330/// `super.name` — a method from the parent's prototype, or a getter's result.
331fn b_super_get(vm: &mut VM, _: u8) -> Value {
332    let name = sval(&vm.pop());
333    match with_host(|h| h.super_resolve(&name)) {
334        host::SuperRef::Data(v) => v,
335        host::SuperRef::Getter(getter) => {
336            let this = with_host(|h| h.current_this());
337            match host::invoke(&getter, Vec::new(), this) {
338                Ok(v) => v,
339                Err(e) => abort(vm, e),
340            }
341        }
342    }
343}
344
345/// Close every loop iterator parked on `vm`'s stack at the op now executing,
346/// innermost first. Called where a chunk is about to be halted abruptly, since
347/// the code that would ordinarily close them is being jumped over.
348///
349/// A close runs user code (a generator's `finally`), which can itself throw; the
350/// error is deliberately dropped, because it must not replace the completion
351/// that caused the unwind.
352fn close_parked_iters(vm: &mut VM) {
353    let n = host::parked_iters(vm);
354    if n == 0 {
355        return;
356    }
357    // The completion that caused the unwind is already pending on the host.
358    // Closing an iterator resumes ANOTHER generator, which settles its own
359    // signal/error state, so the pending one is saved across the close and put
360    // back — otherwise the outer `.return()` would be lost.
361    let saved = with_host(|h| (h.signal.take(), h.error.take()));
362    for _ in 0..n {
363        let it = vm.pop();
364        let _ = close_iterator(&it);
365    }
366    with_host(|h| {
367        h.signal = saved.0;
368        h.error = saved.1;
369    });
370}
371
372fn b_yield(vm: &mut VM, _: u8) -> Value {
373    let v = vm.pop();
374    match host::gen_yield(v) {
375        Ok(sent) => {
376            // A `.return()`/`.throw()` injected on resume sets a pending Return
377            // signal (or error); halt the chunk so the body unwinds through any
378            // enclosing `try/finally`, exactly like a source `return`/`throw`.
379            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
380                // Halting jumps past the loop exits, so the `for…of` / `yield*`
381                // iterators parked on this chunk's stack would be abandoned
382                // still-suspended. They sit directly beneath the yielded value
383                // (innermost last), and the compiler recorded how many are
384                // there for this exact op.
385                close_parked_iters(vm);
386                vm.ip = vm.chunk.ops.len();
387            }
388            sent
389        }
390        // An injected `.throw()` comes back as an error rather than a signal,
391        // and abandons the parked iterators the same way. The thrown value is
392        // already on the host as `exc`; `close_parked_iters` puts back whatever
393        // it saves, so the close cannot swallow it.
394        Err(e) => {
395            close_parked_iters(vm);
396            abort(vm, e)
397        }
398    }
399}
400
401/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
402///
403/// It called `JsHost::property_key` directly, which is the primitive-only half
404/// of the conversion, so an object key never ran `ToPrimitive`:
405/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
406/// member form `a[o] = 1` — which does go through `host::to_property_key` —
407/// keyed on `"TS"`. The two forms are the same abstract operation and now share
408/// the same implementation.
409fn b_propkey(vm: &mut VM, _: u8) -> Value {
410    let v = vm.pop();
411    match host::to_property_key(&v) {
412        Ok(k) => with_host(|h| h.new_str(k)),
413        Err(e) => abort(vm, e),
414    }
415}
416
417fn b_new_target(_vm: &mut VM, _: u8) -> Value {
418    with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
419}
420
421/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
422/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
423/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
424/// `0/0 === NaN`, so `/` is lowered here instead.
425///
426/// Being a builtin rather than a native op means it does NOT reach the numeric
427/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
428/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
429/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
430/// hook now, so `/` coerces exactly as `*` and `-` do.
431fn b_div(vm: &mut VM, _: u8) -> Value {
432    let b = vm.pop();
433    let a = vm.pop();
434    let r = numeric_hook(NumOp::Div, &a, &b);
435    finish(vm, r)
436}
437
438/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
439/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
440/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
441/// one code path that already handles it.
442fn b_pow(vm: &mut VM, _: u8) -> Value {
443    let b = vm.pop();
444    let a = vm.pop();
445    let r = numeric_hook(NumOp::Pow, &a, &b);
446    finish(vm, r)
447}
448
449/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
450fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
451    let excluded = vm.pop();
452    let obj = vm.pop();
453    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
454        .unwrap_or_default()
455        .iter()
456        .map(|v| with_host(|h| h.str_of(v)))
457        .collect();
458    with_host(|h| {
459        let props: IndexMap<String, Value> = match h.get(&obj) {
460            Some(JsObj::Object(m)) => m
461                .iter()
462                .filter(|(k, _)| !excl.contains(k))
463                .map(|(k, v)| (k.clone(), v.clone()))
464                .collect(),
465            _ => IndexMap::new(),
466        };
467        h.new_object(props)
468    })
469}
470
471// ── helpers ──────────────────────────────────────────────────────────────────
472
473fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
474    let mut v = Vec::with_capacity(n);
475    for _ in 0..n {
476        v.push(vm.pop());
477    }
478    v.reverse();
479    v
480}
481
482/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
483fn sval(v: &Value) -> String {
484    if let Value::Str(s) = v {
485        return (**s).clone();
486    }
487    with_host(|h| h.as_str(v)).unwrap_or_default()
488}
489
490/// The same string, without `sval`'s deep copy. Every identifier the compiler
491/// emits is a `Value::Str` constant, so a variable read or write that went
492/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
493/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
494/// handle is a refcount bump instead.
495fn sname(v: &Value) -> std::sync::Arc<String> {
496    match v {
497        Value::Str(s) => s.clone(),
498        _ => std::sync::Arc::new(sval(v)),
499    }
500}
501
502fn abort(vm: &mut VM, e: String) -> Value {
503    with_host(|h| h.error = Some(e));
504    vm.ip = vm.chunk.ops.len();
505    Value::Undef
506}
507
508/// Halt the chunk if a call left an error or non-local signal pending.
509fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
510    match r {
511        Ok(v) => {
512            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
513                vm.ip = vm.chunk.ops.len();
514            }
515            v
516        }
517        Err(e) => abort(vm, e),
518    }
519}
520
521// ── name handlers ─────────────────────────────────────────────────────────────
522
523/// The value a bare global identifier resolves to, or `None` if unbound.
524///
525/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
526/// which must agree: a name reachable one way and not the other is exactly the
527/// discrepancy that left `globalThis.process` undefined while `process` worked.
528pub(crate) fn global_binding(name: &str) -> Option<Value> {
529    if let Some(v) = with_host(|h| h.read_name(name)) {
530        return Some(v);
531    }
532    // Globals bound lazily: numeric sentinels + builtin namespaces.
533    match name {
534        "undefined" => return Some(Value::Undef),
535        "NaN" => return Some(Value::Float(f64::NAN)),
536        "Infinity" => return Some(Value::Float(f64::INFINITY)),
537        // One object, not a fresh one per read: `globalThis === globalThis` is
538        // `true` in JS, and `globalThis.x = 1` is readable back as
539        // `globalThis.x`. Both were false while each read minted a new object.
540        // `global` is Node's alias for the same object.
541        "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
542        _ => {}
543    }
544    if is_namespace(name) || is_known_builtin(name) {
545        return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
546    }
547    None
548}
549
550fn b_getlocal(vm: &mut VM, _: u8) -> Value {
551    let name = sname(&vm.pop());
552    match global_binding(&name) {
553        Some(v) => v,
554        None => abort(vm, host::ref_error(&name)),
555    }
556}
557
558/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
559/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
560/// code — and, either way, never rebinds the name.
561const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
562
563fn readonly_global_error(name: &str) -> String {
564    host::type_error(&format!(
565        "Cannot assign to read only property '{name}' of object '#<Object>'"
566    ))
567}
568
569fn b_setlocal(vm: &mut VM, _: u8) -> Value {
570    let val = vm.pop();
571    let name = sname(&vm.pop());
572    // Sloppy assignment to a non-writable global is DISCARDED, not applied:
573    // `undefined = 1` used to rebind the name and make every later `undefined`
574    // read back as `1`.
575    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
576        return val;
577    }
578    // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
579    // immutable binding). This used to succeed silently.
580    if !with_host(|h| h.set_name(&name, val.clone())) {
581        return abort(vm, host::type_error("Assignment to constant variable."));
582    }
583    val
584}
585
586/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
587/// where sloppy code silently creates a global, strict code throws
588/// `ReferenceError: x is not defined`.
589///
590/// A separate opcode rather than a runtime flag: strictness is a static property
591/// of the code, so the compiler already knows which of the two an assignment is
592/// and sloppy code — everything in a CommonJS module without the directive —
593/// keeps the exact instruction it had.
594fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
595    let val = vm.pop();
596    let name = sname(&vm.pop());
597    if !binding_exists(&name) {
598        return abort(vm, host::ref_error(&name));
599    }
600    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
601        return abort(vm, readonly_global_error(&name));
602    }
603    if !with_host(|h| h.set_name(&name, val.clone())) {
604        return abort(vm, host::type_error("Assignment to constant variable."));
605    }
606    val
607}
608
609/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
610/// materialised builtin namespace. `global_binding` answers the same question
611/// but ALLOCATES the namespace object to do it, which an assignment then throws
612/// away.
613fn binding_exists(name: &str) -> bool {
614    if with_host(|h| h.has_name(name)) {
615        return true;
616    }
617    matches!(
618        name,
619        "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
620    ) || is_namespace(name)
621        || is_known_builtin(name)
622}
623
624fn b_declare(vm: &mut VM, _: u8) -> Value {
625    let val = vm.pop();
626    let name = sname(&vm.pop());
627    with_host(|h| h.declare_name(&name, val.clone()));
628    val
629}
630
631/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
632/// assignment to the name throws instead of overwriting it.
633fn b_declare_const(vm: &mut VM, _: u8) -> Value {
634    let val = vm.pop();
635    let name = sname(&vm.pop());
636    with_host(|h| h.declare_const_name(&name, val.clone()));
637    val
638}
639
640/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
641/// open block scopes, so the name outlives the block it was written in.
642/// `var` hoisting: create the binding as `undefined` unless it already exists.
643fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
644    let name = sname(&vm.pop());
645    with_host(|h| h.hoist_var_name(&name));
646    Value::Undef
647}
648
649fn b_declare_var(vm: &mut VM, _: u8) -> Value {
650    let val = vm.pop();
651    let name = sname(&vm.pop());
652    with_host(|h| h.declare_var_name(&name, val.clone()));
653    val
654}
655
656fn b_push_scope(_: &mut VM, _: u8) -> Value {
657    with_host(|h| h.push_scope());
658    Value::Undef
659}
660
661fn b_pop_scope(_: &mut VM, _: u8) -> Value {
662    with_host(|h| h.pop_scope());
663    Value::Undef
664}
665
666fn b_copy_scope(_: &mut VM, _: u8) -> Value {
667    with_host(|h| h.copy_scope());
668    Value::Undef
669}
670
671fn b_delname(vm: &mut VM, _: u8) -> Value {
672    let name = sval(&vm.pop());
673    with_host(|h| h.del_name(&name));
674    Value::Bool(true)
675}
676
677fn b_this(_vm: &mut VM, _: u8) -> Value {
678    with_host(|h| h.current_this().unwrap_or(Value::Undef))
679}
680
681fn b_load_null(_vm: &mut VM, _: u8) -> Value {
682    with_host(|h| h.null())
683}
684
685// ── attribute / item handlers ─────────────────────────────────────────────────
686
687fn b_getattr(vm: &mut VM, _: u8) -> Value {
688    let name = sval(&vm.pop());
689    let recv = vm.pop();
690    match get_property(&recv, &name) {
691        Ok(v) => v,
692        Err(e) => abort(vm, e),
693    }
694}
695
696/// Read `recv.name` (also the computed-key path for string keys). Walks own
697/// properties, accessors, and the prototype chain (class methods / getters).
698/// Read one small piece out of `recv`'s heap cell under a short borrow.
699///
700/// The closure must not call back into the host (`with_host` is a `RefCell`
701/// borrow and re-entering panics) — which is exactly why it hands back only the
702/// value needed: the caller re-enters freely afterwards. This replaces the old
703/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
704/// `String` just to look at it.
705fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
706    with_host(|h| h.get(recv).and_then(f))
707}
708
709/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
710/// reaches it without a closer link already owning `name`.
711///
712/// A proxy prototype answers only from the position it occupies in the chain: a
713/// nearer prototype that owns the key (as a data property or an accessor) still
714/// wins, exactly as `OrdinaryGet` walks one link at a time.
715pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
716    with_host(|h| {
717        let mut cur = h.proto_of(recv);
718        for _ in 0..100 {
719            let p = cur?;
720            match h.get(&p) {
721                Some(JsObj::Proxy { .. }) => return Some(p),
722                Some(JsObj::Object(props)) if props.contains_key(name) => return None,
723                _ => {}
724            }
725            if h.own_accessor(&p, name).is_some() {
726                return None;
727            }
728            cur = h.proto_of(&p);
729        }
730        None
731    })
732}
733
734pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
735    // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
736    // when the receiver carries no such private element — it does NOT read back
737    // as `undefined`, which is what `C.prototype.method.call({})` used to do.
738    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
739        return Err(private_brand_message(name, false));
740    }
741    get_property_recv(recv, name, recv)
742}
743
744/// The `TypeError` a failed private brand check raises. Node words it two ways:
745/// a private METHOD or accessor names the class the receiver should have been an
746/// instance of, while a private FIELD names the member.
747pub fn private_brand_message(name: &str, writing: bool) -> String {
748    if with_host(|h| h.is_private_method(name)) {
749        if let Some(class) = with_host(|h| h.current_home_class_name()) {
750            return host::type_error(&format!("Receiver must be an instance of class {class}"));
751        }
752    }
753    let verb = if writing { "write" } else { "read" };
754    let prep = if writing { "to" } else { "from" };
755    host::type_error(&format!(
756        "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
757    ))
758}
759
760/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
761/// from and is what a getter sees as `this`; it differs from `recv` only when the
762/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
763/// and a Proxy `get` trap's third argument both need it. Every ordinary read
764/// passes `recv` itself.
765pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
766    // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
767    // target. Checked before anything else so no ordinary-object shortcut can
768    // read past the handler.
769    if let Some(v) = crate::proxy::get(recv, name, receiver)? {
770        return Ok(v);
771    }
772    if with_host(|h| h.is_nullish(recv)) {
773        return Err(host::type_error(&format!(
774            "Cannot read properties of {} (reading '{name}')",
775            with_host(|h| h.str_of(recv))
776        )));
777    }
778    // A read off `globalThis` for a name the object does not own falls back to
779    // the same lazy global binding the bare identifier gets. Without it the
780    // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
781    // and `.JSON` were all `undefined`, so `process === globalThis.process` was
782    // `false` and any `globalThis.X` feature probe reported the feature missing.
783    if with_host(|h| h.is_global_object(recv)) {
784        let own = with_host(|h| match h.get(recv) {
785            Some(JsObj::Object(p)) => p.contains_key(name),
786            _ => false,
787        });
788        // The CommonJS wrapper's parameters are function locals in Node, not
789        // global-object properties: `typeof globalThis.require` is `undefined`
790        // there even though the bare `require` works.
791        const CJS_WRAPPER_LOCALS: &[&str] = &[
792            "require",
793            "module",
794            "exports",
795            "__filename",
796            "__dirname",
797            "__cjs_require",
798            "__cjs_resolve",
799        ];
800        if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
801            if let Some(v) = global_binding(name) {
802                return Ok(v);
803            }
804        }
805    }
806    // Accessor (own or inherited getter) takes precedence over the chain walk.
807    // The getter runs with the RECEIVER as `this`, not the object that owns it.
808    if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
809        return match getter {
810            Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
811            None => Ok(Value::Undef), // set-only property reads as undefined
812        };
813    }
814    // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
815    // one expose it to a plain read, not just to `Object.prototype.toString` —
816    // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
817    // inherits `'Uint8Array'` from the typed-array prototype it now really has.
818    // Anything the receiver's own chain provides wins (a class may define its
819    // own getter), so this is only the fallback.
820    if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
821        if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
822            return Ok(with_host(|h| h.new_str(tag)));
823        }
824    }
825    // `constructor`: a user class/function sets it on the prototype chain, and
826    // that wins; otherwise every builtin instance reports its native
827    // constructor (so `[].constructor`, `new Map().constructor`,
828    // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
829    if name == "constructor" {
830        if let Some(v) = with_host(|h| {
831            match h.get(recv) {
832                Some(JsObj::Object(p)) => p.get("constructor").cloned(),
833                _ => None,
834            }
835            .or_else(|| host::lookup_chain(h, recv, "constructor"))
836        }) {
837            return Ok(v);
838        }
839        if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
840            return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
841        }
842    }
843    // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
844    // answers for EVERY object that inherits from it, not only plain ones —
845    // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
846    // so an array, function or builtin instance read `undefined`. An object with
847    // a null prototype inherits no such accessor and reads `undefined`, which is
848    // why this is skipped there rather than answering `null`.
849    if name == "__proto__"
850        && !with_host(|h| h.has_null_proto(recv))
851        && peek(recv, |o| match o {
852            JsObj::Object(p) => Some(p.contains_key("__proto__")),
853            _ => Some(false),
854        }) != Some(true)
855    {
856        return Ok(prototype_of(recv));
857    }
858    let kind = with_host(|h| h.kind_of(recv));
859    Ok(match kind {
860        Some(ObjKind::Object) => {
861            let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
862            // Typed-array element read (`ta[i]`): elements live in a hidden
863            // `@@elems`, not as own numeric props, so intercept integer keys.
864            if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
865                if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
866                    return Ok(v);
867                }
868            }
869            // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
870            // own numeric props, so integer keys read through to it.
871            if numeric
872                && peek(recv, |o| match o {
873                    JsObj::Object(p) => Some(p.contains_key("@@bytes")),
874                    _ => None,
875                })
876                .unwrap_or(false)
877            {
878                return Ok(crate::stdlib::buffer::byte_get(recv, name));
879            }
880            if let Some(v) = peek(recv, |o| match o {
881                JsObj::Object(p) => p.get(name).cloned(),
882                _ => None,
883            }) {
884                v
885            } else if let Some(link) = proxy_proto_link(recv, name) {
886                // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
887                // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
888                // receiver, so the trap sees the child as `receiver` and `this`
889                // inside a trap-served getter resolves to the child, not the
890                // proxy. `lookup_chain` cannot do this: it reads property maps,
891                // and a proxy has none.
892                return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
893            } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
894                // A method / data property inherited from the prototype chain.
895                v
896            } else if crate::stdlib::native_tag(recv)
897                .map(|tag| crate::stdlib::instance_has_method(&tag, name))
898                .unwrap_or(false)
899            {
900                // A native instance method read as a property (`server.listen`) →
901                // a bound method, dispatched via `instance_call` when invoked.
902                bound_method(recv, name)
903            } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
904                // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
905                // read as `undefined` there — which is also what makes
906                // `Object.create(null) + 1` the spec `TypeError` instead of a
907                // silent `"[object Object]1"`.
908                bound_method(recv, name)
909            } else {
910                Value::Undef
911            }
912        }
913        Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
914            function_property(recv, name)
915        }
916        Some(ObjKind::Symbol) => match name {
917            "description" => {
918                match peek(recv, |o| match o {
919                    JsObj::Symbol { desc, .. } => desc.clone(),
920                    _ => None,
921                }) {
922                    Some(d) => with_host(|h| h.new_str(d)),
923                    None => Value::Undef,
924                }
925            }
926            "toString" => bound_method(recv, name),
927            _ => Value::Undef,
928        },
929        Some(ObjKind::BigInt) => {
930            if matches!(
931                name,
932                "toString" | "valueOf" | "toLocaleString" | "constructor"
933            ) {
934                bound_method(recv, name)
935            } else {
936                Value::Undef
937            }
938        }
939        Some(ObjKind::RegExp) => {
940            // A RegExp holds no collection, so cloning the compiled pattern here
941            // does not scale with any input size; `regexp_property` re-enters the
942            // host to allocate `source`/`flags`, so it cannot run under a borrow.
943            let r = peek(recv, |o| match o {
944                JsObj::RegExp(r) => Some(r.clone()),
945                _ => None,
946            });
947            match r {
948                Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
949                    if crate::regexp::is_regexp_method(name) {
950                        bound_method(recv, name)
951                    } else {
952                        Value::Undef
953                    }
954                }),
955                None => Value::Undef,
956            }
957        }
958        // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
959        // the read must be `undefined` rather than a live count.
960        Some(ObjKind::Map) => {
961            let (len, weak) = peek(recv, |o| match o {
962                JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
963                _ => None,
964            })
965            .unwrap_or((0, false));
966            match name {
967                "size" if !weak => Value::Float(len as f64),
968                "@@iterator" => bound_method(recv, name),
969                _ if is_map_method(name) => bound_method(recv, name),
970                _ => Value::Undef,
971            }
972        }
973        Some(ObjKind::Set) => {
974            let (len, weak) = peek(recv, |o| match o {
975                JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
976                _ => None,
977            })
978            .unwrap_or((0, false));
979            match name {
980                "size" if !weak => Value::Float(len as f64),
981                "@@iterator" => bound_method(recv, name),
982                _ if is_set_method(name) => bound_method(recv, name),
983                _ => Value::Undef,
984            }
985        }
986        Some(ObjKind::Generator) => {
987            if is_generator_method(name) {
988                bound_method(recv, name)
989            } else {
990                Value::Undef
991            }
992        }
993        Some(ObjKind::Promise) => {
994            if matches!(name, "then" | "catch" | "finally") {
995                bound_method(recv, name)
996            } else {
997                Value::Undef
998            }
999        }
1000        Some(ObjKind::Iter) => {
1001            if matches!(name, "next" | "return" | "@@iterator") {
1002                bound_method(recv, name)
1003            } else {
1004                Value::Undef
1005            }
1006        }
1007        Some(ObjKind::Array) => {
1008            if name == "length" {
1009                let n = peek(recv, |o| match o {
1010                    JsObj::Array(items) => Some(items.len()),
1011                    _ => None,
1012                })
1013                .unwrap_or(0);
1014                Value::Float(n as f64)
1015            } else if let Ok(i) = name.parse::<usize>() {
1016                peek(recv, |o| match o {
1017                    JsObj::Array(items) => items.get(i).cloned(),
1018                    _ => None,
1019                })
1020                .unwrap_or(Value::Undef)
1021            } else if name == "@@iterator" || is_array_method(name) || is_object_method(name) {
1022                bound_method(recv, name)
1023            } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1024                // Extra own props attached to an array (e.g. `RegExp.exec` result's
1025                // `.index`/`.input`/`.groups`).
1026                v
1027            } else {
1028                Value::Undef
1029            }
1030        }
1031        Some(ObjKind::Str) => {
1032            // `.length` and `s[i]` count UTF-16 code units, not code points.
1033            if name == "length" {
1034                let n = peek(recv, |o| match o {
1035                    JsObj::Str(s) => Some(crate::utf16::len(s)),
1036                    _ => None,
1037                })
1038                .unwrap_or(0);
1039                Value::Float(n as f64)
1040            } else if let Ok(i) = name.parse::<usize>() {
1041                match peek(recv, |o| match o {
1042                    JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1043                    _ => None,
1044                }) {
1045                    Some(c) => with_host(|h| h.new_str(c)),
1046                    None => Value::Undef,
1047                }
1048            } else if name == "@@iterator" || is_string_method(name) {
1049                bound_method(recv, name)
1050            } else {
1051                Value::Undef
1052            }
1053        }
1054        Some(ObjKind::Builtin) => {
1055            let ns = peek(recv, |o| match o {
1056                JsObj::Builtin(ns) => Some(ns.clone()),
1057                _ => None,
1058            })
1059            .unwrap_or_default();
1060            namespace_property(&ns, name)
1061        }
1062        _ => {
1063            // Primitive numbers/booleans: method access -> bound method.
1064            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1065                bound_method(recv, name)
1066            } else {
1067                Value::Undef
1068            }
1069        }
1070    })
1071}
1072
1073/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1074/// object literal because the module cache is the single source of truth: a
1075/// populated copy would answer reads correctly and silently ignore a `delete`,
1076/// which is the operation the property exists for.
1077pub const REQUIRE_CACHE: &str = "__cjs_cache";
1078
1079/// The builtin constructor name for a value with no own/inherited `constructor`
1080/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1081/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1082/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1083/// whose `.constructor.name` is `""` in Node — not worth modelling).
1084fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1085    match h.get(recv) {
1086        Some(JsObj::Array(_)) => Some("Array"),
1087        Some(JsObj::Object(props)) => {
1088            // A native instance reports its own constructor, not Object — e.g.
1089            // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1090            // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1091            // the `@@native` tag off the already-borrowed host (calling
1092            // `native_tag`, which re-enters `with_host`, would double-borrow).
1093            match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1094                Some("Buffer") => Some("Buffer"),
1095                Some("URL") => Some("URL"),
1096                Some("Date") => Some("Date"),
1097                Some("WeakRef") => Some("WeakRef"),
1098                Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1099                Some("TextEncoder") => Some("TextEncoder"),
1100                Some("TextDecoder") => Some("TextDecoder"),
1101                Some("EventEmitter") => Some("EventEmitter"),
1102                Some("Timeout") => Some("Timeout"),
1103                Some("Immediate") => Some("Immediate"),
1104                _ => Some("Object"),
1105            }
1106        }
1107        Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1108        Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1109        Some(JsObj::Promise { .. }) => Some("Promise"),
1110        Some(JsObj::Str(_)) => Some("String"),
1111        Some(JsObj::Symbol { .. }) => Some("Symbol"),
1112        Some(JsObj::BigInt(_)) => Some("BigInt"),
1113        Some(JsObj::RegExp(_)) => Some("RegExp"),
1114        Some(JsObj::Iter { .. }) => Some("Iterator"),
1115        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
1116            Some("Function")
1117        }
1118        _ => match recv {
1119            Value::Float(_) | Value::Int(_) => Some("Number"),
1120            Value::Bool(_) => Some("Boolean"),
1121            _ => None,
1122        },
1123    }
1124}
1125
1126/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1127/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1128/// `process`), whose `.name` is `undefined` in Node.
1129///
1130/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1131/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1132/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1133fn is_builtin_ctor(name: &str) -> bool {
1134    matches!(
1135        name,
1136        "Array"
1137            | "Object"
1138            | "Number"
1139            | "String"
1140            | "Boolean"
1141            | "Symbol"
1142            | "Function"
1143            | "Map"
1144            | "Set"
1145            | "WeakMap"
1146            | "WeakSet"
1147            | "Promise"
1148            | "BigInt"
1149            | "Iterator"
1150            | "RegExp"
1151            | "Date"
1152            | "ArrayBuffer"
1153            | "Uint8Array"
1154            | "Int8Array"
1155            | "Uint8ClampedArray"
1156            | "Int16Array"
1157            | "Uint16Array"
1158            | "Int32Array"
1159            | "Uint32Array"
1160            | "Float32Array"
1161            | "Float64Array"
1162            | "BigInt64Array"
1163            | "BigUint64Array"
1164            | "WeakRef"
1165            | "FinalizationRegistry"
1166            | "TextEncoder"
1167            | "TextDecoder"
1168            | "IncomingMessage"
1169            | "ServerResponse"
1170            | "EventEmitter"
1171            | "Buffer"
1172            | "URL"
1173            | "URLSearchParams"
1174            | "Timeout"
1175            | "Immediate"
1176    ) || host::ERROR_NAMES.contains(&name)
1177}
1178
1179fn bound_method(recv: &Value, name: &str) -> Value {
1180    with_host(|h| {
1181        h.alloc(JsObj::BoundMethod {
1182            recv: recv.clone(),
1183            name: name.to_string(),
1184        })
1185    })
1186}
1187
1188/// `Object.prototype` methods reachable on any object.
1189fn is_object_method(name: &str) -> bool {
1190    matches!(
1191        name,
1192        "hasOwnProperty"
1193            | "isPrototypeOf"
1194            | "propertyIsEnumerable"
1195            | "toString"
1196            | "toLocaleString"
1197            | "valueOf"
1198            | "constructor"
1199    )
1200}
1201
1202/// The `Object.prototype` methods installed as thunks on the real
1203/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1204/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1205pub const OBJECT_PROTO_METHODS: &[&str] = &[
1206    "hasOwnProperty",
1207    "isPrototypeOf",
1208    "propertyIsEnumerable",
1209    "toString",
1210    "toLocaleString",
1211    "valueOf",
1212];
1213
1214pub fn is_object_builtin_method(name: &str) -> bool {
1215    matches!(
1216        name,
1217        "hasOwnProperty"
1218            | "isPrototypeOf"
1219            | "propertyIsEnumerable"
1220            | "toString"
1221            | "toLocaleString"
1222            | "valueOf"
1223    )
1224}
1225
1226/// Dispatch an `Object.prototype` builtin method on an object/instance.
1227pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1228    match name {
1229        "hasOwnProperty" => {
1230            let k = with_host(|h| h.property_key(&arg0(&args)));
1231            // A builtin namespace/prototype receiver (`Map.prototype`) reports
1232            // ownership via `has_property` (its methods resolve as thunks).
1233            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
1234                return Ok(Value::Bool(has_property(recv, &k)?));
1235            }
1236            // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
1237            // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
1238            // the target's property map.
1239            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
1240                let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
1241                return Ok(Value::Bool(!matches!(d, Value::Undef)));
1242            }
1243            // A Buffer's / typed array's own keys are its element indices: the
1244            // `length`/`byteLength` slots are internal bookkeeping, and V8
1245            // reports `hasOwnProperty('length')` as false for a typed array.
1246            // Shared with the `in` operator so the two cannot drift apart.
1247            if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
1248                return Ok(Value::Bool(hit));
1249            }
1250            let has = with_host(|h| match h.get(recv) {
1251                Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
1252                Some(JsObj::Array(items)) => {
1253                    k == "length"
1254                        || k.parse::<usize>()
1255                            .map(|i| i < items.len() && !h.is_hole(recv, i))
1256                            .unwrap_or(false)
1257                }
1258                _ => false,
1259            });
1260            Ok(Value::Bool(has))
1261        }
1262        "isPrototypeOf" => {
1263            let target = arg0(&args);
1264            // The ARGUMENT is what gets walked, so a proxy there needs its
1265            // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
1266            // proxy does not hold, which reported `false` for every proxy. From
1267            // the second hop on the chain is ordinary objects again, walked by
1268            // the recorded link exactly as before.
1269            let mut cur = match crate::proxy::get_prototype_of(&target)? {
1270                Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
1271                None => with_host(|h| h.proto_of(&target)),
1272            };
1273            while let Some(p) = cur {
1274                if with_host(|h| h.strict_eq(&p, recv)) {
1275                    return Ok(Value::Bool(true));
1276                }
1277                cur = with_host(|h| h.proto_of(&p));
1278            }
1279            Ok(Value::Bool(false))
1280        }
1281        "propertyIsEnumerable" => {
1282            let k = with_host(|h| h.str_of(&arg0(&args)));
1283            // Own *and* enumerable — a non-enumerable own slot reads false. On a
1284            // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
1285            // trap, since there is no property map to enumerate.
1286            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
1287                let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
1288                return Ok(Value::Bool(has));
1289            }
1290            let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
1291            Ok(Value::Bool(has))
1292        }
1293        "toString" => Ok(with_host(|h| {
1294            // An instance with a custom `toString` up the chain is handled by
1295            // call_method before reaching here; this is the default.
1296            let s = h.str_of(recv);
1297            h.new_str(s)
1298        })),
1299        // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
1300        // `Invoke(this, "toString")` — no locale behavior of its own. It was
1301        // installed as a thunk on `Object.prototype` but had no dispatch arm, so
1302        // calling it threw `is not a function` on every plain object.
1303        "toLocaleString" => {
1304            let v = host::call_method(recv, "toString", Vec::new())?;
1305            Ok(v)
1306        }
1307        "valueOf" => Ok(recv.clone()),
1308        _ => Err(host::type_error(&format!("{name} is not a function"))),
1309    }
1310}
1311
1312/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
1313/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
1314/// these (so the caller can try statics).
1315pub fn function_builtin_method(
1316    recv: &Value,
1317    name: &str,
1318    args: &[Value],
1319) -> Result<Option<Value>, String> {
1320    match name {
1321        "call" => {
1322            let this = args.first().cloned();
1323            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
1324            Ok(Some(host::invoke(recv, rest, this)?))
1325        }
1326        "apply" => {
1327            let this = args.first().cloned();
1328            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
1329            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
1330                Vec::new()
1331            } else {
1332                with_host(|h| h.iter_vec(&arr)).unwrap_or_default()
1333            };
1334            Ok(Some(host::invoke(recv, call_args, this)?))
1335        }
1336        "bind" => {
1337            let this = args.first().cloned().unwrap_or(Value::Undef);
1338            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
1339            Ok(Some(with_host(|h| {
1340                h.alloc(JsObj::BoundFunc {
1341                    target: recv.clone(),
1342                    this,
1343                    args: pre,
1344                })
1345            })))
1346        }
1347        "toString" => Ok(Some(with_host(|h| {
1348            let s = h.str_of(recv);
1349            h.new_str(s)
1350        }))),
1351        _ => Ok(None),
1352    }
1353}
1354
1355fn is_function_method(name: &str) -> bool {
1356    matches!(name, "call" | "apply" | "bind" | "toString")
1357}
1358fn is_map_method(name: &str) -> bool {
1359    matches!(
1360        name,
1361        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
1362    )
1363}
1364fn is_set_method(name: &str) -> bool {
1365    matches!(
1366        name,
1367        "add" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
1368    )
1369}
1370fn is_generator_method(name: &str) -> bool {
1371    matches!(name, "next" | "return" | "throw")
1372}
1373
1374/// A property read on a function/class value: own fn-props (statics, name,
1375/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
1376fn function_property(recv: &Value, name: &str) -> Value {
1377    // A class static, inherited down the constructor chain.
1378    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
1379        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
1380            return v;
1381        }
1382        // The chain may bottom out in a BUILTIN constructor (`class D extends
1383        // Array {}`), whose statics `class_static` cannot see — it only walks
1384        // `ClassVal.parent` links between user classes. Finish the lookup with an
1385        // ordinary read on that ancestor so `D.from` inherits `Array.from`.
1386        if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
1387            if let Ok(v) = get_property(&anc, name) {
1388                if !matches!(v, Value::Undef) {
1389                    return v;
1390                }
1391            }
1392        }
1393    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1394        return v;
1395    }
1396    // A method inherited via the function's [[Prototype]] chain (set with
1397    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
1398    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
1399    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1400        return v;
1401    }
1402    match name {
1403        "name" => with_host(|h| {
1404            let n = h.callable_name(recv);
1405            h.new_str(n)
1406        }),
1407        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
1408        "prototype" => ensure_fn_prototype(recv),
1409        _ if is_function_method(name) => bound_method(recv, name),
1410        _ => Value::Undef,
1411    }
1412}
1413
1414/// The `.prototype` of a function value, auto-created on first access (as Node
1415/// does for every non-arrow function) with `.constructor` linking back. Arrow
1416/// functions have no `prototype`.
1417fn ensure_fn_prototype(recv: &Value) -> Value {
1418    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
1419        return p;
1420    }
1421    // Only a constructor gets one: an arrow, a method definition and an async
1422    // function are not constructors, and a class sets its own (10.2.5).
1423    if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
1424        return Value::Undef;
1425    }
1426    if !with_host(|h| h.owns_prototype(recv)) {
1427        return Value::Undef;
1428    }
1429    with_host(|h| {
1430        let proto = h.new_object(IndexMap::new());
1431        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
1432            p.insert("constructor".to_string(), recv.clone());
1433        }
1434        h.hide_prop(&proto, "constructor");
1435        h.set_fn_prop(recv, "prototype", proto.clone());
1436        proto
1437    })
1438}
1439
1440/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
1441/// `console.log`).
1442pub fn namespace_property(ns: &str, name: &str) -> Value {
1443    // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
1444    // read sees whatever is loaded now and `delete` (see `delete_property`)
1445    // actually invalidates.
1446    if ns == REQUIRE_CACHE {
1447        return crate::module::cache_get(name).unwrap_or(Value::Undef);
1448    }
1449    // The ENTRY script's `require` is this builtin rather than the per-module
1450    // closure, so its `cache` has to be handed out here too.
1451    if ns == "require" && name == "cache" {
1452        return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
1453    }
1454    // Numeric constants.
1455    let konst = match (ns, name) {
1456        ("Math", "PI") => Some(std::f64::consts::PI),
1457        ("Math", "E") => Some(std::f64::consts::E),
1458        ("Math", "LN2") => Some(std::f64::consts::LN_2),
1459        ("Math", "LN10") => Some(std::f64::consts::LN_10),
1460        ("Math", "LOG2E") => Some(std::f64::consts::LOG2_E),
1461        ("Math", "LOG10E") => Some(std::f64::consts::LOG10_E),
1462        ("Math", "SQRT2") => Some(std::f64::consts::SQRT_2),
1463        ("Math", "SQRT1_2") => Some(std::f64::consts::FRAC_1_SQRT_2),
1464        ("Number", "MAX_SAFE_INTEGER") => Some(9007199254740991.0),
1465        ("Number", "MIN_SAFE_INTEGER") => Some(-9007199254740991.0),
1466        ("Number", "MAX_VALUE") => Some(f64::MAX),
1467        // The smallest positive value a Number can hold, which is the smallest
1468        // SUBNORMAL double (`5e-324`), not Rust's `f64::MIN_POSITIVE` — that is
1469        // the smallest *normal* double, `2.2250738585072014e-308`, ~256 binary
1470        // orders of magnitude too large.
1471        ("Number", "MIN_VALUE") => Some(f64::from_bits(1)),
1472        ("Number", "EPSILON") => Some(f64::EPSILON),
1473        ("Number", "POSITIVE_INFINITY") => Some(f64::INFINITY),
1474        ("Number", "NEGATIVE_INFINITY") => Some(f64::NEG_INFINITY),
1475        ("Number", "NaN") => Some(f64::NAN),
1476        _ => None,
1477    };
1478    if let Some(k) = konst {
1479        return Value::Float(k);
1480    }
1481    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
1482    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
1483    // `undefined`.
1484    if name == "name" && is_builtin_ctor(ns) {
1485        return with_host(|h| h.new_str(ns.to_string()));
1486    }
1487    // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
1488    // computed property/method key.
1489    if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
1490        return with_host(|h| h.well_known_symbol(name));
1491    }
1492    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
1493    // `buffer.Buffer`, `url.URL`).
1494    if let Some(v) = crate::stdlib::constant(ns, name) {
1495        return v;
1496    }
1497    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
1498    // `Array.prototype`, …): a prototype namespace whose methods are callable
1499    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
1500    // `get-intrinsic`/`function-bind` family).
1501    if name == "prototype" && is_builtin_ctor(ns) {
1502        // Same reasoning as the native prototypes below, for the error
1503        // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
1504        // `error_protos` object, so `Error.prototype` has to read back that same
1505        // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
1506        // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
1507        // Error("x")) === Error.prototype` was false, and `typeof
1508        // Error.prototype` was `"function"` where node says `"object"`.
1509        if host::ERROR_NAMES.contains(&ns) {
1510            if let Some(p) = with_host(|h| {
1511                h.ensure_error_protos();
1512                host::error_proto_of(h, ns)
1513            }) {
1514                return p;
1515            }
1516        }
1517        // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
1518        // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
1519        // Buffer.prototype` must compare equal, which a freshly-allocated
1520        // `Builtin` handle never can.
1521        if let Some(p) = with_host(|h| {
1522            h.ensure_native_protos();
1523            h.native_proto(ns)
1524        }) {
1525            return p;
1526        }
1527        let _ = ns;
1528        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
1529    }
1530    // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
1531    // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
1532    // above never fired and the read produced `undefined` — which broke the ES5
1533    // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
1534    // reads `StringDecoder.prototype.end` at load, and threw
1535    // `Cannot read properties of undefined (reading 'end')`. Built from the same
1536    // instance-method table a method read consults, so the two cannot disagree.
1537    if name == "prototype" {
1538        if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
1539            return p;
1540        }
1541    }
1542    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
1543    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
1544    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
1545    if let Some(ctor) = ns.strip_suffix(".prototype") {
1546        return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
1547    }
1548    let qualified = format!("{ns}.{name}");
1549    if is_known_builtin(&qualified) {
1550        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
1551    }
1552    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
1553    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
1554        return v;
1555    }
1556    Value::Undef
1557}
1558
1559/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
1560/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
1561/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
1562/// libraries type-check on; every other method routes through normal method
1563/// dispatch on `recv`.
1564pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
1565    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
1566    // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
1567    // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
1568    if ctor == "Error" && method == "toString" {
1569        let s = with_host(|h| h.error_to_string(recv)).unwrap_or_else(|| {
1570            with_host(|h| {
1571                let name = host::lookup_chain(h, recv, "name")
1572                    .map(|n| h.str_of(&n))
1573                    .unwrap_or_else(|| "Error".into());
1574                let msg = host::lookup_chain(h, recv, "message")
1575                    .map(|m| h.str_of(&m))
1576                    .unwrap_or_default();
1577                if msg.is_empty() {
1578                    name
1579                } else {
1580                    format!("{name}: {msg}")
1581                }
1582            })
1583        });
1584        return Ok(with_host(|h| h.new_str(s)));
1585    }
1586    if ctor == "Object" && method == "toString" {
1587        // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
1588        // (own or inherited, data property or getter) replaces the builtin brand,
1589        // which is how a class advertises its own (`class C { get
1590        // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
1591        // runs outside the host borrow so an accessor can be invoked.
1592        // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
1593        // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
1594        // the ordinary receiver does, to keep the read off objects that have no
1595        // tag) would always miss and brand every tagged proxy `[object Object]`.
1596        let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1597            || with_host(|h| {
1598                host::lookup_chain(h, recv, "@@toStringTag").is_some()
1599                    || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1600            });
1601        if tagged {
1602            let t = get_property(recv, "@@toStringTag")?;
1603            if let Some(s) = with_host(|h| h.as_str(&t)) {
1604                return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
1605            }
1606        }
1607        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
1608    }
1609    // These thunks now live on the real `Object.prototype` object, i.e. on the
1610    // receiver's own chain — routing back through `call_method` would re-resolve
1611    // this very thunk and recurse.
1612    if ctor == "Object" && is_object_builtin_method(method) {
1613        return object_builtin_method(recv, method, args);
1614    }
1615    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
1616    // emitter method directly against `recv` (routing back through `call_method`
1617    // would re-resolve the mixed-in thunk and recurse).
1618    if ctor == "EventEmitter" {
1619        return crate::stdlib::events::instance_call(recv, method, args);
1620    }
1621    // Same recursion hazard for the exotics with a real prototype object: the
1622    // thunk now lives ON the receiver's prototype chain, so `call_method` would
1623    // re-resolve this very thunk. Dispatch straight to the native instance
1624    // implementation when the receiver is in fact an instance of `ctor`.
1625    if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
1626        return crate::stdlib::buffer::instance_call(recv, method, &args);
1627    }
1628    // The shared typed-array methods now live on the `%TypedArray%.prototype`
1629    // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
1630    // appears for anything read directly off `Uint8Array.prototype`. Both
1631    // dispatch the same way, and both must bypass `call_method` or the thunk
1632    // would re-resolve itself off the receiver's chain and recurse.
1633    if ctor == "Uint8Array" || ctor == "TypedArray" {
1634        match crate::stdlib::native_tag(recv).as_deref() {
1635            Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
1636            Some("TypedArray") => {
1637                return crate::stdlib::typedarray::instance_call(recv, method, &args)
1638            }
1639            _ => {}
1640        }
1641    }
1642    // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
1643    // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
1644    // `LengthOfArrayLike`), which is what makes
1645    // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
1646    // is not an Array, so `call_method` would report the method missing.
1647    if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
1648        return array_generic(recv, method, args);
1649    }
1650    // The general form of the two special cases above: a thunk taken off a native
1651    // constructor's real prototype, invoked with a receiver that IS an instance of
1652    // that constructor. Routing back through `call_method` would re-resolve this
1653    // very thunk off the receiver's own chain and recurse forever, which is why
1654    // each such prototype needed a hand-written bypass; now they all have one.
1655    if crate::stdlib::native_tag(recv).as_deref() == Some(ctor) {
1656        return crate::stdlib::instance_call(ctor, recv, method, args);
1657    }
1658    host::call_method(recv, method, args)
1659}
1660
1661/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
1662/// one, or `None` when reading that symbol must yield `undefined`.
1663///
1664/// Every builtin brand is already computed in exactly one place (`object_tag`),
1665/// so this reuses it and subtracts the legacy builtins, which brand for
1666/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
1667/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
1668/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
1669/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
1670/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
1671/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
1672/// `TextEncoder`/`TextDecoder` all read their brand.
1673fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
1674    // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
1675    // of which `object_tag` already brands.
1676    let tag = object_brand(h, v);
1677    const NO_TAG: &[&str] = &[
1678        "Undefined",
1679        "Null",
1680        "Boolean",
1681        "Number",
1682        "String",
1683        "Array",
1684        "Function",
1685        "Object",
1686        "Date",
1687        "RegExp",
1688        "Error",
1689    ];
1690    if NO_TAG.contains(&tag.as_str()) {
1691        return None;
1692    }
1693    Some(tag)
1694}
1695
1696/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
1697/// Every builtin exotic object reports its own brand, which is how packages
1698/// type-test values they did not construct (`toString.call(x) ===
1699/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
1700/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
1701fn object_tag(h: &host::JsHost, v: &Value) -> String {
1702    format!("[object {}]", object_brand(h, v))
1703}
1704
1705/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
1706/// …), without the `[object …]` wrapper. Split out so the brand and the
1707/// `Symbol.toStringTag` property read cannot disagree about what a value is.
1708fn object_brand(h: &host::JsHost, v: &Value) -> String {
1709    let tag: String = match v {
1710        Value::Undef => "Undefined".into(),
1711        Value::Bool(_) => "Boolean".into(),
1712        Value::Int(_) | Value::Float(_) => "Number".into(),
1713        Value::Str(_) => "String".into(),
1714        Value::Obj(_) => match h.get(v) {
1715            Some(JsObj::Null) => "Null".into(),
1716            Some(JsObj::Str(_)) => "String".into(),
1717            Some(JsObj::Array(_)) => "Array".into(),
1718            // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
1719            // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
1720            // {}))` is `'[object Array]'`. Everything else about a proxy brands
1721            // as a plain Object (a `Symbol.toStringTag` read through the `get`
1722            // trap is handled by the caller, before this).
1723            Some(JsObj::Proxy { target, .. }) => {
1724                let mut cur = target;
1725                for _ in 0..100 {
1726                    match h.get(cur) {
1727                        Some(JsObj::Proxy { target: t, .. }) => cur = t,
1728                        _ => break,
1729                    }
1730                }
1731                match h.get(cur) {
1732                    Some(JsObj::Array(_)) => "Array".into(),
1733                    _ => "Object".into(),
1734                }
1735            }
1736            // `function*` / `async function` / `async function*` carry their own
1737            // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
1738            Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
1739                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
1740                Some(d) if d.is_generator => "GeneratorFunction".into(),
1741                Some(d) if d.is_async => "AsyncFunction".into(),
1742                _ => "Function".into(),
1743            },
1744            // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
1745            // brand by name (21.3.1.9, 25.5.3, 28.1.14).
1746            Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
1747                n.clone()
1748            }
1749            Some(JsObj::Class(_))
1750            | Some(JsObj::Builtin(_))
1751            | Some(JsObj::BoundFunc { .. })
1752            | Some(JsObj::BoundMethod { .. }) => "Function".into(),
1753            // A suspended generator object is `[object Generator]`; an async one
1754            // `[object AsyncGenerator]`.
1755            Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
1756            Some(JsObj::Generator { .. }) => "Generator".into(),
1757            Some(JsObj::RegExp(_)) => "RegExp".into(),
1758            Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
1759            Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
1760            Some(JsObj::Promise { .. }) => "Promise".into(),
1761            Some(JsObj::Symbol { .. }) => "Symbol".into(),
1762            Some(JsObj::BigInt(_)) => "BigInt".into(),
1763            // Native-tagged instances brand by their tag; a typed array brands by
1764            // its element kind (`@@kind`), and every Error subclass is `Error`.
1765            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
1766                Some("TypedArray") => p
1767                    .get("@@kind")
1768                    .map(|k| h.str_of(k))
1769                    .unwrap_or_else(|| "Uint8Array".into()),
1770                Some("Buffer") => "Uint8Array".into(),
1771                // Every native class that really carries a `Symbol.toStringTag`
1772                // in Node brands by its own name. Verified against node v26:
1773                // `Object.prototype.toString.call(new WeakRef({}))` is
1774                // `[object WeakRef]`. The rest of the `@@native` tags
1775                // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
1776                // classes with NO tag, so they stay `[object Object]` — listing
1777                // them here would invent a brand Node does not have.
1778                Some(
1779                    t @ ("ArrayBuffer"
1780                    | "DataView"
1781                    | "Date"
1782                    | "WeakRef"
1783                    | "FinalizationRegistry"
1784                    | "TextEncoder"
1785                    | "TextDecoder"
1786                    | "URL"
1787                    | "URLSearchParams"),
1788                ) => t.into(),
1789                _ if h.error_to_string(v).is_some() => "Error".into(),
1790                _ => "Object".into(),
1791            },
1792            _ => "Object".into(),
1793        },
1794        // node-js only produces the Value variants above; fusevm's shell-oriented
1795        // variants never arise here.
1796        _ => "Object".into(),
1797    };
1798    tag
1799}
1800
1801fn b_setattr(vm: &mut VM, _: u8) -> Value {
1802    let val = vm.pop();
1803    let name = sval(&vm.pop());
1804    let recv = vm.pop();
1805    if let Err(e) = set_property(&recv, &name, val.clone()) {
1806        return abort(vm, e);
1807    }
1808    val
1809}
1810
1811/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
1812/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
1813/// `class C { static [k] = function(){} }`.
1814///
1815/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
1816/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
1817/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
1818/// unconditionally here.
1819///
1820/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
1821/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
1822fn b_named_eval(vm: &mut VM, _: u8) -> Value {
1823    let func = vm.pop();
1824    let kind = vm.pop().to_int();
1825    let key = vm.pop();
1826    let key = sval(&key);
1827    // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
1828    // description gives the empty name, not `[undefined]`.
1829    let base = match with_host(|h| h.symbol_of_key(&key)) {
1830        Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
1831            Some(JsObj::Symbol {
1832                desc: Some(desc), ..
1833            }) => format!("[{desc}]"),
1834            _ => String::new(),
1835        },
1836        None => key,
1837    };
1838    let name = match kind {
1839        host::member::GET => format!("get {base}"),
1840        host::member::SET => format!("set {base}"),
1841        _ => base,
1842    };
1843    with_host(|h| {
1844        let s = h.new_str(name);
1845        h.set_fn_prop(&func, "name", s);
1846    });
1847    func
1848}
1849
1850/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
1851/// on the same path a plain `o.k = v` takes.
1852pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
1853    set_property(recv, name, val)
1854}
1855
1856fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
1857    // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
1858    // element. The class's own field initializers install theirs directly
1859    // (`host::init_one_field`), so a declaration never reaches this check.
1860    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
1861        return Err(private_brand_message(name, true));
1862    }
1863    // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
1864    if crate::proxy::set(recv, name, &val, recv)? {
1865        return Ok(());
1866    }
1867    // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
1868    // back. Writing only the own property left the two views disagreeing:
1869    // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
1870    if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
1871        with_host(|h| h.set_name(name, val.clone()));
1872    }
1873    // `obj.__proto__ = p` re-links the prototype — but only for the two values
1874    // the Annex B setter accepts, an Object or `null`. Everything else is a
1875    // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
1876    // untouched and creates no own key), and a null-prototype object inherits
1877    // no such setter at all, so there the assignment is an ORDINARY own
1878    // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
1879    // prototype to the number 5.
1880    if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
1881        if with_host(|h| h.has_null_proto(recv)) {
1882            // falls through to the ordinary own-property write below
1883        } else {
1884            let assignable =
1885                with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
1886            if assignable {
1887                with_host(|h| h.set_proto(recv, val));
1888            }
1889            return Ok(());
1890        }
1891    }
1892    // A non-writable own property, or a new key on a non-extensible object,
1893    // silently discards the write (sloppy mode — the mode every script runs in).
1894    if !with_host(|h| h.can_write_prop(recv, name)) {
1895        return Ok(());
1896    }
1897    // An inherited/own setter accessor intercepts the write.
1898    if let Some((_, Some(setter))) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1899        let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
1900        return Ok(());
1901    }
1902    // A set-only-elsewhere getter (accessor with no setter): ignore the write.
1903    if let Some((Some(_), None)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1904        return Ok(());
1905    }
1906    // Writing `name`/`prototype`/statics on a function value.
1907    if matches!(
1908        with_host(|h| h.kind_of(recv)),
1909        Some(ObjKind::Func) | Some(ObjKind::Class)
1910    ) {
1911        with_host(|h| h.set_fn_prop(recv, name, val));
1912        return Ok(());
1913    }
1914    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
1915    // Each bare reference is a fresh `Builtin` handle, so route to the stable
1916    // per-namespace side table rather than the per-index `fn_props`.
1917    if let Some(ns) = peek(recv, |o| match o {
1918        JsObj::Builtin(ns) => Some(ns.clone()),
1919        _ => None,
1920    }) {
1921        // `process.exitCode` is an accessor in Node, not a data property: the
1922        // setter validates and stores the code the process will finally exit
1923        // with. Landing it in the generic static table made it a write-only
1924        // decoration — `process.exitCode = 3` read back as 3 and the process
1925        // still exited 0.
1926        if ns == "process" && name == "exitCode" {
1927            return crate::stdlib::process::set_exit_code(&val);
1928        }
1929        with_host(|h| h.set_builtin_static(&ns, name, val));
1930        return Ok(());
1931    }
1932    // `re.lastIndex = n` on a RegExp advances/resets its match cursor.
1933    if name == "lastIndex" {
1934        if let Some(n) = with_host(|h| match h.get(recv) {
1935            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
1936            _ => None,
1937        }) {
1938            with_host(|h| {
1939                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
1940                    r.last_index = if n.is_finite() && n >= 0.0 {
1941                        crate::utf16::U16Index::new(n as usize)
1942                    } else {
1943                        crate::utf16::U16Index::ZERO
1944                    };
1945                }
1946            });
1947            return Ok(());
1948        }
1949    }
1950    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
1951    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
1952        let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
1953        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
1954            return Ok(());
1955        }
1956        // `buf[i] = n` writes through to the Buffer's hidden byte array.
1957        if crate::stdlib::buffer::byte_set(recv, name, &val) {
1958            return Ok(());
1959        }
1960    }
1961    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
1962    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
1963        && name != "length"
1964        && name.parse::<usize>().is_err()
1965    {
1966        with_host(|h| h.set_fn_prop(recv, name, val));
1967        return Ok(());
1968    }
1969    // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
1970    // and does so outside the host borrow because `ToNumber` may run a user
1971    // `valueOf`. An invalid length throws instead of being silently coerced to 0.
1972    let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
1973        Some(host::to_array_length(&val)?)
1974    } else {
1975        None
1976    };
1977    with_host(|h| match h.get_mut(recv) {
1978        Some(JsObj::Object(props)) => {
1979            // Adding a *new* array-index key must re-place it into ascending
1980            // integer-key order (updating an existing key keeps its position).
1981            let is_new = !props.contains_key(name);
1982            props.insert(name.to_string(), val);
1983            if is_new && host::array_index(name).is_some() {
1984                host::canonicalize_own_keys(props);
1985            }
1986        }
1987        Some(JsObj::Array(items)) => {
1988            if let Some(n) = new_len {
1989                // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
1990                // just the one own key); shrinking drops any hole past the end.
1991                let old = items.len();
1992                items.resize(n, Value::Undef);
1993                if n > old {
1994                    h.mark_hole_range(recv, old..n);
1995                } else {
1996                    h.truncate_holes(recv, n);
1997                }
1998            } else if let Ok(i) = name.parse::<usize>() {
1999                // A write PAST the end leaves the skipped positions elided.
2000                let old = items.len();
2001                if i >= old {
2002                    items.resize(i + 1, Value::Undef);
2003                }
2004                items[i] = val;
2005                if i > old {
2006                    h.mark_hole_range(recv, old..i);
2007                }
2008                // …and the written index itself is no longer one. This is the
2009                // single site that keeps a hole record from outliving the
2010                // elision it describes: every array element write in the
2011                // language reaches it.
2012                h.clear_hole(recv, i);
2013            }
2014        }
2015        _ => {}
2016    });
2017    Ok(())
2018}
2019
2020fn b_getitem(vm: &mut VM, _: u8) -> Value {
2021    let idx = vm.pop();
2022    let recv = vm.pop();
2023    let key = match host::to_property_key(&idx) {
2024        Ok(k) => k,
2025        Err(e) => return abort(vm, e),
2026    };
2027    match get_property(&recv, &key) {
2028        Ok(v) => v,
2029        Err(e) => abort(vm, e),
2030    }
2031}
2032
2033fn b_setitem(vm: &mut VM, _: u8) -> Value {
2034    let val = vm.pop();
2035    let idx = vm.pop();
2036    let recv = vm.pop();
2037    let key = match host::to_property_key(&idx) {
2038        Ok(k) => k,
2039        Err(e) => return abort(vm, e),
2040    };
2041    if let Err(e) = set_property(&recv, &key, val.clone()) {
2042        return abort(vm, e);
2043    }
2044    val
2045}
2046
2047/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
2048/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
2049/// the three cannot drift. Reports `false` for a non-configurable property
2050/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
2051/// which is also what deleting an absent key reports.
2052pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
2053    // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
2054    // throw — the reason this reports a `Result` rather than a bare `bool`.
2055    if let Some(b) = crate::proxy::delete(recv, key)? {
2056        return Ok(b);
2057    }
2058    // `delete require.cache[id]` drops the module so the next `require` of that
2059    // file runs it again — the whole point of exposing the cache.
2060    if peek(recv, |o| match o {
2061        JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
2062        _ => None,
2063    }) == Some(true)
2064    {
2065        return Ok(crate::module::cache_delete(key));
2066    }
2067    if !with_host(|h| h.prop_attrs(recv, key).configurable) {
2068        return Ok(false);
2069    }
2070    with_host(|h| {
2071        let index = key.parse::<usize>();
2072        match h.get_mut(recv) {
2073            Some(JsObj::Object(props)) => {
2074                props.shift_remove(key);
2075                return;
2076            }
2077            Some(JsObj::Array(items)) => {
2078                if let Ok(i) = index {
2079                    if i < items.len() {
2080                        // `delete a[i]` punches a HOLE: the length is unchanged
2081                        // but the index stops being an own property.
2082                        items[i] = Value::Undef;
2083                        h.mark_hole(recv, i);
2084                    }
2085                    return;
2086                }
2087            }
2088            _ => {}
2089        }
2090        // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
2091        // a function/class, is an ordinary own property kept in the side table.
2092        h.remove_fn_prop(recv, key);
2093    });
2094    Ok(true)
2095}
2096
2097fn b_delitem(vm: &mut VM, _: u8) -> Value {
2098    let idx = vm.pop();
2099    let recv = vm.pop();
2100    // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
2101    // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
2102    // description and delete a key nothing ever wrote.
2103    let key = match host::to_property_key(&idx) {
2104        Ok(k) => k,
2105        Err(e) => return abort(vm, e),
2106    };
2107    match delete_property(&recv, &key) {
2108        Ok(b) => Value::Bool(b),
2109        Err(e) => abort(vm, e),
2110    }
2111}
2112
2113fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
2114    let name = sval(&vm.pop());
2115    let recv = vm.pop();
2116    match delete_property(&recv, &name) {
2117        Ok(b) => Value::Bool(b),
2118        Err(e) => abort(vm, e),
2119    }
2120}
2121
2122// ── constructors ──────────────────────────────────────────────────────────────
2123
2124fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
2125    let parts = pop_n(vm, argc as usize);
2126    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
2127    with_host(|h| h.new_str(s))
2128}
2129
2130fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
2131    let items = pop_n(vm, argc as usize);
2132    with_host(|h| h.new_array(items))
2133}
2134
2135/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
2136/// only for an array literal that actually contains an elision, so a dense
2137/// literal costs nothing. Returns `undefined`; the array stays on the stack
2138/// underneath (the compiler `Dup`s it).
2139fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
2140    let idx = vm.pop();
2141    let arr = vm.pop();
2142    let i = match idx {
2143        Value::Int(i) if i >= 0 => i as usize,
2144        _ => return Value::Undef,
2145    };
2146    with_host(|h| h.mark_hole(&arr, i));
2147    Value::Undef
2148}
2149
2150fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
2151    let flat = pop_n(vm, argc as usize);
2152    let mut props: IndexMap<String, Value> = IndexMap::new();
2153    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
2154    let mut proto_override: Option<Value> = None;
2155    let mut i = 0;
2156    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
2157        if i + 2 >= flat.len() {
2158            break;
2159        }
2160        // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
2161        // the literal reserves its slot here with the `@@ord:` marker key that
2162        // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
2163        // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
2164        // marker landed at the end.
2165        if matches!(flat[i], Value::Int(2)) {
2166            let key = with_host(|h| h.str_of(&flat[i + 1]));
2167            props
2168                .entry(format!("{}{key}", host::ORD_MARKER))
2169                .or_insert(Value::Undef);
2170            i += 3;
2171            continue;
2172        }
2173        let spread = matches!(flat[i], Value::Int(1));
2174        if spread {
2175            let src = flat[i + 1].clone();
2176            // A STRING source spreads its index properties (`{..."ab"}` is
2177            // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
2178            // String exotic object owns one enumerable property per UTF-16 code
2179            // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
2180            // a string source contributed nothing and `{..."ab"}` was `{}`.
2181            // Every other primitive (number/boolean/symbol) boxes to an object
2182            // with no own enumerable properties, and null/undefined are ignored,
2183            // so those correctly stay no-ops on the path below.
2184            if let Some(s) = with_host(|h| h.as_str(&src)) {
2185                for idx in 0..crate::utf16::len(&s) {
2186                    if let Ok(ch) = get_property(&src, &idx.to_string()) {
2187                        props.insert(idx.to_string(), ch);
2188                    }
2189                }
2190                i += 3;
2191                continue;
2192            }
2193            // Object spread copies own *enumerable* properties only — never the
2194            // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
2195            // into something that still claimed to be a Buffer) and never a
2196            // property a descriptor marked non-enumerable.
2197            let entries = host::own_enum_entries_deep(&src);
2198            for (k, v) in entries {
2199                props.insert(k, v);
2200            }
2201            // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
2202            // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
2203            for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
2204                props.insert(k, v);
2205            }
2206        } else {
2207            let key = with_host(|h| h.str_of(&flat[i + 1]));
2208            if key == "__proto__" {
2209                proto_override = Some(flat[i + 2].clone());
2210            } else {
2211                props.insert(key, flat[i + 2].clone());
2212            }
2213        }
2214        i += 3;
2215    }
2216    with_host(|h| {
2217        let o = h.new_object(props);
2218        if let Some(p) = proto_override {
2219            if matches!(p, Value::Obj(_)) {
2220                h.set_proto(&o, p);
2221            }
2222        }
2223        o
2224    })
2225}
2226
2227fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
2228    let def_id = match vm.pop() {
2229        Value::Int(n) => n as usize,
2230        Value::Float(f) => f as usize,
2231        _ => return abort(vm, "internal: MKFUNC id".into()),
2232    };
2233    let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
2234        Some(d) => (
2235            d.is_arrow,
2236            (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
2237        ),
2238        None => (false, None),
2239    });
2240    with_host(|h| {
2241        let mut env = h.current_env_capture();
2242        let this = h.current_this();
2243        // A named function expression closes over an extra scope holding its own
2244        // name, so the body can recurse through it (`function f(){ … f() … }`)
2245        // independently of whatever the outer binding is later set to.
2246        if self_name.is_some() {
2247            env = host::child_env(env);
2248        }
2249        let f = h.alloc(JsObj::Func(FuncVal {
2250            def_id,
2251            env: Some(env.clone()),
2252            this,
2253            is_arrow,
2254            home_class: None,
2255        }));
2256        if let Some(n) = self_name {
2257            env.borrow_mut().vars.insert(n, f.clone());
2258        }
2259        f
2260    })
2261}
2262
2263// ── truthiness / coercion / equality ──────────────────────────────────────────
2264
2265fn b_truthy(vm: &mut VM, _: u8) -> Value {
2266    let v = vm.pop();
2267    Value::Bool(with_host(|h| h.truthy(&v)))
2268}
2269
2270fn b_nullish(vm: &mut VM, _: u8) -> Value {
2271    let v = vm.pop();
2272    Value::Bool(with_host(|h| h.is_nullish(&v)))
2273}
2274
2275fn b_tostr(vm: &mut VM, _: u8) -> Value {
2276    let v = vm.pop();
2277    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
2278    // `String(x)`, object keys).
2279    match host::to_string_value(&v) {
2280        Ok(s) => s,
2281        Err(e) => abort(vm, e),
2282    }
2283}
2284
2285fn b_typeof(vm: &mut VM, _: u8) -> Value {
2286    let v = vm.pop();
2287    with_host(|h| {
2288        let t = h.type_of(&v);
2289        h.new_str(t)
2290    })
2291}
2292
2293/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
2294/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
2295fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
2296    let name = sval(&vm.pop());
2297    // Bound name (user variable) → typeof its value.
2298    if let Some(v) = with_host(|h| h.read_name(&name)) {
2299        return with_host(|h| {
2300            let t = h.type_of(&v);
2301            h.new_str(t)
2302        });
2303    }
2304    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
2305    // would produce, then take its type (so object-namespaces like `console`/
2306    // `Math`/`JSON`/`process` report "object", constructors report "function").
2307    let t = match name.as_str() {
2308        "undefined" => "undefined".to_string(),
2309        "NaN" | "Infinity" => "number".to_string(),
2310        "globalThis" | "global" => "object".to_string(),
2311        n if is_namespace(n) || is_known_builtin(n) => {
2312            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
2313            with_host(|h| h.type_of(&v)).to_string()
2314        }
2315        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
2316    };
2317    with_host(|h| h.new_str(t))
2318}
2319
2320fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
2321    let b = vm.pop();
2322    let a = vm.pop();
2323    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
2324}
2325
2326fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
2327    let b = vm.pop();
2328    let a = vm.pop();
2329    // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
2330    // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
2331    // runs before the host borrow. Object ⇄ object stays a reference check.
2332    let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
2333        (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
2334            Ok(p) => (p, b),
2335            Err(e) => return abort(vm, e),
2336        },
2337        (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
2338            Ok(p) => (a, p),
2339            Err(e) => return abort(vm, e),
2340        },
2341        _ => (a, b),
2342    };
2343    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
2344}
2345
2346fn b_instanceof(vm: &mut VM, _: u8) -> Value {
2347    let ctor = vm.pop();
2348    let obj = vm.pop();
2349    match host::instance_of(&obj, &ctor) {
2350        Ok(b) => Value::Bool(b),
2351        Err(e) => abort(vm, e),
2352    }
2353}
2354
2355// ── bitwise / unary ───────────────────────────────────────────────────────────
2356
2357fn b_binop(vm: &mut VM, _: u8) -> Value {
2358    let b = vm.pop();
2359    let a = vm.pop();
2360    let tag = match vm.pop() {
2361        Value::Int(n) => n,
2362        _ => 0,
2363    };
2364    // Both operands are ToPrimitive-d with the number hint before ToInt32
2365    // (ECMA-262 13.12.1), which has to happen outside the host borrow.
2366    let r = host::to_primitive(&a, "number")
2367        .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
2368        .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
2369    finish(vm, r)
2370}
2371
2372fn b_unary(vm: &mut VM, _: u8) -> Value {
2373    let v = vm.pop();
2374    let tag = match vm.pop() {
2375        Value::Int(n) => n,
2376        _ => 0,
2377    };
2378    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
2379    // computed in arbitrary precision.
2380    if with_host(|h| h.is_bigint_val(&v)) {
2381        return match tag {
2382            host::unop::POS => abort(
2383                vm,
2384                host::type_error("Cannot convert a BigInt value to a number"),
2385            ),
2386            host::unop::BITNOT => {
2387                let b = with_host(|h| h.as_bigint(&v)).unwrap();
2388                let r = -(b + num_bigint::BigInt::from(1));
2389                with_host(|h| h.new_bigint(r))
2390            }
2391            _ => Value::Undef,
2392        };
2393    }
2394    // `ToNumber` outside the host borrow: an object operand's `valueOf` /
2395    // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
2396    let n = match host::to_number_value(&v) {
2397        Ok(n) => n,
2398        Err(e) => return abort(vm, e),
2399    };
2400    match tag {
2401        host::unop::POS => Value::Float(n),
2402        host::unop::BITNOT => {
2403            let i = if n.is_finite() {
2404                n.trunc() as i64 as i32
2405            } else {
2406                0
2407            };
2408            Value::Float(!i as f64)
2409        }
2410        _ => Value::Undef,
2411    }
2412}
2413
2414// ── membership ────────────────────────────────────────────────────────────────
2415
2416fn b_contains(vm: &mut VM, _: u8) -> Value {
2417    let container = vm.pop();
2418    let key = vm.pop();
2419    // `x in y` requires y to be an object. V8 names both operands:
2420    // `Cannot use 'in' operator to search for 'a' in 5`.
2421    if !matches!(container, Value::Obj(_)) {
2422        let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
2423        return abort(
2424            vm,
2425            host::type_error(&format!(
2426                "Cannot use 'in' operator to search for '{k}' in {c}"
2427            )),
2428        );
2429    }
2430    let k = with_host(|h| h.property_key(&key));
2431    match has_property(&container, &k) {
2432        Ok(b) => Value::Bool(b),
2433        Err(e) => abort(vm, e),
2434    }
2435}
2436
2437// ── control ───────────────────────────────────────────────────────────────────
2438
2439fn b_sig_return(vm: &mut VM, _: u8) -> Value {
2440    let v = vm.pop();
2441    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
2442    vm.ip = vm.chunk.ops.len();
2443    v
2444}
2445
2446/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
2447/// inside a `try` block, which the host runs as its own chunk). Raise the signal
2448/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
2449fn b_sig_break(vm: &mut VM, _: u8) -> Value {
2450    let label = sval(&vm.pop());
2451    let label = (!label.is_empty()).then_some(label);
2452    with_host(|h| h.signal = Some(host::Signal::Break(label)));
2453    vm.ip = vm.chunk.ops.len();
2454    Value::Undef
2455}
2456
2457/// `continue [label]` out of a `try` block — see [`b_sig_break`].
2458fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
2459    let label = sval(&vm.pop());
2460    let label = (!label.is_empty()).then_some(label);
2461    with_host(|h| h.signal = Some(host::Signal::Continue(label)));
2462    vm.ip = vm.chunk.ops.len();
2463    Value::Undef
2464}
2465
2466/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
2467/// describes what the `try` is nested in (see [`host::unwind`]):
2468///
2469/// * no signal → `NONE`, execution continues normally;
2470/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
2471///   keeps travelling outward;
2472/// * `break`/`continue` targeting the enclosing loop → consume it and report
2473///   `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
2474///   continue target;
2475/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
2476///   the signal pending, so leaving this loop re-dispatches it one level out.
2477fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
2478    let cont_tag = sval(&vm.pop());
2479    let brk_tag = sval(&vm.pop());
2480    let sig = match with_host(|h| h.signal.clone()) {
2481        Some(s) => s,
2482        None => return Value::Int(host::unwind::NONE),
2483    };
2484    // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
2485    let propagate = |vm: &mut VM| {
2486        vm.ip = vm.chunk.ops.len();
2487        Value::Int(host::unwind::NONE)
2488    };
2489    match &sig {
2490        host::Signal::Return(_) => propagate(vm),
2491        host::Signal::Break(label) => {
2492            if brk_tag == host::unwind::NO_LOOP {
2493                return propagate(vm);
2494            }
2495            let mine = match label {
2496                None => true, // unlabeled: always the innermost enclosing context
2497                Some(l) => brk_tag == *l,
2498            };
2499            if mine {
2500                with_host(|h| h.signal = None);
2501            }
2502            // Not ours: still leave this context by its break exit, keeping the
2503            // signal pending for the next dispatch point one level out.
2504            Value::Int(host::unwind::BREAK)
2505        }
2506        host::Signal::Continue(label) => {
2507            let mine = match label {
2508                // Unlabeled `continue` binds to the innermost continue-catching
2509                // loop — which a `switch` between here and it is NOT.
2510                None => cont_tag != host::unwind::NO_LOOP,
2511                Some(l) => cont_tag == *l,
2512            };
2513            if mine {
2514                with_host(|h| h.signal = None);
2515                return Value::Int(host::unwind::CONTINUE);
2516            }
2517            if brk_tag == host::unwind::NO_LOOP {
2518                return propagate(vm);
2519            }
2520            // The target loop is further out: exit the innermost context here and
2521            // re-dispatch there.
2522            Value::Int(host::unwind::BREAK)
2523        }
2524    }
2525}
2526
2527fn b_throw(vm: &mut VM, _: u8) -> Value {
2528    let v = vm.pop();
2529    let msg = with_host(|h| {
2530        h.exc = Some(v.clone());
2531        // Prefer an error object's message for the top-level report.
2532        error_display(h, &v)
2533    });
2534    abort(vm, msg)
2535}
2536
2537fn error_display(h: &host::JsHost, v: &Value) -> String {
2538    if let Some(JsObj::Object(props)) = h.get(v) {
2539        let name = props
2540            .get("name")
2541            .map(|x| h.str_of(x))
2542            .unwrap_or_else(|| "Error".into());
2543        if let Some(m) = props.get("message") {
2544            return format!("Uncaught {name}: {}", h.str_of(m));
2545        }
2546    }
2547    format!("Uncaught {}", h.str_of(v))
2548}
2549
2550fn b_try(vm: &mut VM, _: u8) -> Value {
2551    let id = match vm.pop() {
2552        Value::Int(n) => n as usize,
2553        _ => return abort(vm, "internal: TRY id".into()),
2554    };
2555    // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
2556    // its handler and its finalizer bytecode — every time control entered it,
2557    // which for a `try` inside a loop is once per iteration.
2558    let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
2559        Some(t) => t,
2560        None => return abort(vm, "internal: unknown try id".into()),
2561    };
2562    let mut pending: Option<String> = None;
2563    // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
2564    // through can leave block scopes open. Snapshot the scope and restore it
2565    // before the handler and after the whole statement.
2566    let scope = with_host(|h| h.scope_snapshot());
2567
2568    with_host(|h| h.push_scope()); // the try block is its own block scope
2569    let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
2570        with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
2571    });
2572    with_host(|h| h.restore_scope(scope.clone()));
2573    let signal_after = with_host(|h| h.signal.is_some());
2574    if let Err(e) = body_res {
2575        if signal_after {
2576            pending = Some(e);
2577        } else if has_handler {
2578            // Bind the thrown value (or a synthesized error) to the catch param.
2579            let thrown =
2580                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
2581            with_host(|h| {
2582                h.error = None;
2583                h.exc = None;
2584            });
2585            // The catch parameter is block-scoped to the handler.
2586            with_host(|h| h.push_scope());
2587            if let Some(name) = &catch_bind {
2588                with_host(|h| h.declare_name(name, thrown));
2589            }
2590            let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
2591                with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
2592            });
2593            with_host(|h| h.restore_scope(scope.clone()));
2594            if let Err(e2) = hres {
2595                pending = Some(e2);
2596            }
2597        } else {
2598            pending = Some(e);
2599        }
2600    }
2601
2602    // finally always runs; a finally error/signal supersedes.
2603    if has_finalizer {
2604        let sig_before = with_host(|h| h.signal.take());
2605        with_host(|h| h.push_scope()); // ditto for `finally`
2606        let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
2607            with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
2608        });
2609        with_host(|h| h.restore_scope(scope.clone()));
2610        match fres {
2611            Ok(_) => {
2612                if with_host(|h| h.signal.is_none()) {
2613                    // The finalizer completed normally: the try/catch block's own
2614                    // abrupt completion resumes.
2615                    with_host(|h| h.signal = sig_before);
2616                } else {
2617                    // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
2618                    // completion is abrupt (`return`/`break`/`continue` inside
2619                    // `finally`), that completion REPLACES the try/catch block's —
2620                    // including a pending throw, which is discarded, not rethrown.
2621                    pending = None;
2622                    with_host(|h| {
2623                        h.error = None;
2624                        h.exc = None;
2625                    });
2626                }
2627            }
2628            Err(e) => pending = Some(e),
2629        }
2630    }
2631
2632    if let Some(e) = pending {
2633        return abort(vm, e);
2634    }
2635    Value::Undef
2636}
2637
2638/// Synthesize an `Error`-shaped object from an internal error string, linked to
2639/// the matching builtin error prototype so `instanceof`/`.constructor` work.
2640pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
2641    h.ensure_error_protos();
2642    // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
2643    // error class, exactly as Node's internal errors render it in `.stack`.
2644    let (head, rest) = match e.split_once(": ") {
2645        Some((n, m)) => (n, m.to_string()),
2646        None => ("", e.to_string()),
2647    };
2648    let (base, code) = match head.split_once(" [") {
2649        Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
2650        _ => (head, None),
2651    };
2652    let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
2653        (base.to_string(), rest)
2654    } else {
2655        ("Error".to_string(), e.to_string())
2656    };
2657    // A `host::plain_coded_error` marker: the code rides at the head of the
2658    // MESSAGE rather than in the class, because Node's native-layer errors set
2659    // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
2660    // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
2661    // internal and must never reach a user-visible `.message`.
2662    let mut code = code;
2663    // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
2664    // bracketed head, false for the marker form.
2665    let mut bracketed = code.is_some();
2666    if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
2667        if let Some((c, m)) = rest.split_once('\u{1}') {
2668            code = Some(c.to_string());
2669            bracketed = false;
2670            message = m.to_string();
2671        }
2672    }
2673    let mut props: IndexMap<String, Value> = IndexMap::new();
2674    let mv = h.new_str(message.clone());
2675    props.insert("message".into(), mv);
2676    if let Some(c) = &code {
2677        let cv = h.new_str(c.clone());
2678        props.insert("code".into(), cv);
2679        if bracketed {
2680            // Marks this as a Node JS-layer error, whose `toString` brackets the
2681            // code. A native-layer error has the same `.code` and does not.
2682            props.insert("@@nodeError".into(), Value::Bool(true));
2683        }
2684    }
2685    let label = match (&code, bracketed) {
2686        (Some(c), true) => format!("{name} [{c}]"),
2687        _ => name.clone(),
2688    };
2689    let frames = h.stack_frames();
2690    let stack = if message.is_empty() {
2691        format!("{label}{frames}")
2692    } else {
2693        format!("{label}: {message}{frames}")
2694    };
2695    let sv = h.new_str(stack);
2696    props.insert("stack".into(), sv);
2697    // A libuv system-error message is itself the canonical encoding of the
2698    // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
2699    // filesystem/network failure recovers the enumerable `code`/`errno`/
2700    // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
2701    // single most common error-handling idiom in Node packages) depend on.
2702    for (k, v) in syscall_error_fields(&message) {
2703        let sv = match v {
2704            SysField::Str(s) => h.new_str(s),
2705            SysField::Num(n) => Value::Float(n),
2706        };
2707        props.insert(k.into(), sv);
2708    }
2709    let obj = h.new_object(props);
2710    if let Some(p) = host::error_proto_of(h, &name) {
2711        h.set_proto(&obj, p);
2712    }
2713    // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
2714    // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
2715    h.hide_prop(&obj, "message");
2716    h.hide_prop(&obj, "stack");
2717    obj
2718}
2719
2720enum SysField {
2721    Str(String),
2722    Num(f64),
2723}
2724
2725/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
2726/// own properties Node hangs off a system error. Returns empty for any message
2727/// that is not in that shape.
2728fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
2729    let (code, rest) = match message.split_once(": ") {
2730        Some((c, r))
2731            if c.len() >= 2
2732                && c.starts_with('E')
2733                && c.bytes()
2734                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
2735        {
2736            (c, r)
2737        }
2738        _ => return Vec::new(),
2739    };
2740    let mut out: Vec<(&'static str, SysField)> = vec![
2741        ("errno", SysField::Num(errno_for(code))),
2742        ("code", SysField::Str(code.to_string())),
2743    ];
2744    // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
2745    if let Some((_, tail)) = rest.split_once(", ") {
2746        let (syscall, path) = match tail.split_once(" '") {
2747            Some((s, p)) => (s, p.strip_suffix('\'')),
2748            None => (tail, None),
2749        };
2750        out.push(("syscall", SysField::Str(syscall.to_string())));
2751        if let Some(p) = path {
2752            out.push(("path", SysField::Str(p.to_string())));
2753        }
2754    }
2755    out
2756}
2757
2758/// The negative `errno` Node reports for a libuv error code on this platform.
2759/// Only the codes `err_str` can produce are mapped; anything else reports the
2760/// generic `EIO` number rather than inventing a value.
2761fn errno_for(code: &str) -> f64 {
2762    let n: i32 = match code {
2763        "ENOENT" => 2,
2764        "EACCES" => 13,
2765        "EEXIST" => 17,
2766        "ENOTDIR" => 20,
2767        "EISDIR" => 21,
2768        "EINVAL" => 22,
2769        "EPIPE" => 32,
2770        "ENOTEMPTY" => 66,
2771        _ => 5, // EIO
2772    };
2773    -f64::from(n)
2774}
2775
2776// ── iteration ─────────────────────────────────────────────────────────────────
2777
2778fn b_getiter(vm: &mut VM, _: u8) -> Value {
2779    let v = vm.pop();
2780    // A generator is its own iterator (resumed lazily by FORITER).
2781    if with_host(|h| h.is_generator_val(&v)) {
2782        return v;
2783    }
2784    // A Proxy's iterator comes from its traps, materialized eagerly: the
2785    // `lookup_chain` probe below reads the property map a proxy does not have.
2786    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
2787        return match crate::proxy::iterate(&v) {
2788            Ok(Some(items)) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
2789            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
2790            Err(e) => abort(vm, e),
2791        };
2792    }
2793    // An object with a user `Symbol.iterator`: call it to get the iterator object.
2794    if let Some(iter_fn) = with_host(|h| host::lookup_chain(h, &v, "@@iterator")) {
2795        if with_host(|h| host::is_callable(h, &iter_fn)) {
2796            return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
2797                Ok(it) => it,
2798                Err(e) => abort(vm, e),
2799            };
2800        }
2801    }
2802    match with_host(|h| h.iter_vec(&v)) {
2803        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
2804        Err(e) => abort(vm, e),
2805    }
2806}
2807
2808fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
2809    let v = vm.pop();
2810    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
2811    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
2812    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
2813    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
2814        return match crate::proxy::own_enum_string_keys(&v) {
2815            Ok(keys) => with_host(|h| {
2816                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
2817                h.new_array(out)
2818            }),
2819            Err(e) => abort(vm, e),
2820        };
2821    }
2822    let keys = with_host(|h| h.enum_keys(&v));
2823    with_host(|h| h.new_array(keys))
2824}
2825
2826fn b_foriter(vm: &mut VM, _: u8) -> Value {
2827    let it = match vm.stack.last() {
2828        Some(v) => v.clone(),
2829        None => return abort(vm, "internal: FORITER with empty stack".into()),
2830    };
2831    // Eager array-backed iterator (arrays/strings/Map/Set).
2832    let eager = with_host(|h| {
2833        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
2834            if *idx < items.len() {
2835                let v = items[*idx].clone();
2836                *idx += 1;
2837                return Some(Some(v));
2838            }
2839            return Some(None);
2840        }
2841        None
2842    });
2843    if let Some(step) = eager {
2844        return match step {
2845            Some(v) => {
2846                vm.push(v);
2847                Value::Bool(true)
2848            }
2849            None => Value::Bool(false),
2850        };
2851    }
2852    // Generator: resume one step.
2853    if with_host(|h| h.is_generator_val(&it)) {
2854        return match host::gen_resume(&it, Value::Undef) {
2855            Ok(host::GenStep::Yield(v)) => {
2856                vm.push(v);
2857                Value::Bool(true)
2858            }
2859            Ok(host::GenStep::Done(_)) => Value::Bool(false),
2860            Err(e) => abort(vm, e),
2861        };
2862    }
2863    // A user iterator object with a `.next()` returning `{ value, done }`.
2864    match host::call_method(&it, "next", Vec::new()) {
2865        Ok(step) => {
2866            let done = get_property(&step, "done")
2867                .map(|d| with_host(|h| h.truthy(&d)))
2868                .unwrap_or(true);
2869            if done {
2870                Value::Bool(false)
2871            } else {
2872                match get_property(&step, "value") {
2873                    Ok(v) => {
2874                        vm.push(v);
2875                        Value::Bool(true)
2876                    }
2877                    Err(e) => abort(vm, e),
2878                }
2879            }
2880        }
2881        Err(e) => abort(vm, e),
2882    }
2883}
2884
2885fn b_unpack(vm: &mut VM, _: u8) -> Value {
2886    let star = match vm.pop() {
2887        Value::Int(n) => n,
2888        _ => -1,
2889    };
2890    let count = match vm.pop() {
2891        Value::Int(n) => n as usize,
2892        _ => 0,
2893    };
2894    let iterable = vm.pop();
2895    let items = match host::iter_all(&iterable) {
2896        Ok(v) => v,
2897        Err(e) => return abort(vm, e),
2898    };
2899    let ordered: Vec<Value> = if star < 0 {
2900        (0..count)
2901            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
2902            .collect()
2903    } else {
2904        let si = star as usize;
2905        let after = count.saturating_sub(si + 1);
2906        let rest_end = items.len().saturating_sub(after).max(si);
2907        let mut out: Vec<Value> = Vec::with_capacity(count);
2908        for i in 0..si {
2909            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
2910        }
2911        let rest: Vec<Value> = items
2912            .get(si..rest_end)
2913            .map(|s| s.to_vec())
2914            .unwrap_or_default();
2915        out.push(with_host(|h| h.new_array(rest)));
2916        for j in 0..after {
2917            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
2918        }
2919        out
2920    };
2921    if ordered.is_empty() {
2922        return Value::Undef;
2923    }
2924    for it in ordered[1..].iter().rev().cloned() {
2925        vm.push(it);
2926    }
2927    ordered[0].clone()
2928}
2929
2930fn b_build_args(vm: &mut VM, argc: u8) -> Value {
2931    let flat = pop_n(vm, argc as usize);
2932    let mut out = Vec::new();
2933    // Elided positions of an array literal (tag 2), recorded as the run-time
2934    // index each lands on — which only this walk knows, because a preceding
2935    // spread contributes an unknown number of elements. Call-argument lists,
2936    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
2937    // empty for them.
2938    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
2939    let mut i = 0;
2940    while i + 1 < flat.len() {
2941        let val = flat[i + 1].clone();
2942        match flat[i] {
2943            Value::Int(1) => match host::iter_all(&val) {
2944                Ok(items) => out.extend(items),
2945                Err(e) => return abort(vm, e),
2946            },
2947            Value::Int(2) => {
2948                holes.insert(out.len());
2949                out.push(Value::Undef);
2950            }
2951            _ => out.push(val),
2952        }
2953        i += 2;
2954    }
2955    with_host(|h| {
2956        let arr = h.new_array(out);
2957        h.install_holes(&arr, holes);
2958        arr
2959    })
2960}
2961
2962// ── calls ──────────────────────────────────────────────────────────────────────
2963
2964fn b_call(vm: &mut VM, argc: u8) -> Value {
2965    let mut args = pop_n(vm, argc as usize);
2966    let name = sval(&args.remove(0));
2967    let r = host::call_named(&name, args);
2968    // A bare name that resolved to a non-callable reports the VALUE
2969    // (`undefined is not a function`); node names the identifier. Resolving it
2970    // again to learn what the message said costs nothing off the error path.
2971    let r = r.map_err(|e| {
2972        let shown = global_binding(&name)
2973            .map(|v| with_host(|h| h.str_of(&v)))
2974            .unwrap_or_default();
2975        host::name_call_site(vm, &shown, e)
2976    });
2977    finish(vm, r)
2978}
2979
2980/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
2981/// method name. `call_method` resolves by name and bottoms out in
2982/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
2983/// that happens to be a function reported "is not a function". Read the element
2984/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
2985/// A computed call's key is a property key, so it goes through ToPropertyKey:
2986/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
2987/// answers "" for a number, which turned `arr[0]()` into a call to the method
2988/// named "" — so the key is stringified here instead.
2989fn call_key_of(v: &Value) -> String {
2990    if let Value::Str(s) = v {
2991        return (**s).clone();
2992    }
2993    with_host(|h| h.str_of(v))
2994}
2995
2996fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
2997    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
2998        return None;
2999    }
3000    let f = get_property(recv, name).ok()?;
3001    with_host(|h| host::is_callable(h, &f))
3002        .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
3003}
3004
3005fn b_call_method(vm: &mut VM, argc: u8) -> Value {
3006    let mut args = pop_n(vm, argc as usize);
3007    let recv = args.remove(0);
3008    let name = call_key_of(&args.remove(0));
3009    if let Some(r) = index_element_call(&recv, &name, &args) {
3010        return finish(vm, r);
3011    }
3012    let r = host::call_method(&recv, &name, args);
3013    // `z.f()` on a missing method is `z.f is not a function` in node, not
3014    // `f is not a function`: V8 names the callee as the source wrote it. The
3015    // text was recorded for this op at compile time.
3016    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
3017    finish(vm, r)
3018}
3019
3020fn b_call_value(vm: &mut VM, argc: u8) -> Value {
3021    let mut args = pop_n(vm, argc as usize);
3022    let callable = args.remove(0);
3023    let r = host::invoke(&callable, args, None);
3024    // The callee here is an expression, not a name, so the message it produced
3025    // describes the VALUE (`undefined is not a function`); node names the
3026    // expression. Same site table, keyed on that rendering.
3027    let r = r.map_err(|e| {
3028        let shown = with_host(|h| h.str_of(&callable));
3029        host::name_call_site(vm, &shown, e)
3030    });
3031    finish(vm, r)
3032}
3033
3034fn b_new(vm: &mut VM, argc: u8) -> Value {
3035    let mut args = pop_n(vm, argc as usize);
3036    let ctor = args.remove(0);
3037    let r = host::construct(&ctor, args);
3038    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
3039    // call does.
3040    let r = r.map_err(|e| {
3041        let shown = with_host(|h| h.str_of(&ctor));
3042        host::name_call_site(vm, &shown, e)
3043    });
3044    finish(vm, r)
3045}
3046
3047fn b_apply(vm: &mut VM, _: u8) -> Value {
3048    let args_arr = vm.pop();
3049    let callable = vm.pop();
3050    let args = host::iter_all(&args_arr).unwrap_or_default();
3051    let r = host::invoke(&callable, args, None);
3052    finish(vm, r)
3053}
3054
3055fn b_apply_method(vm: &mut VM, _: u8) -> Value {
3056    let args_arr = vm.pop();
3057    let name = call_key_of(&vm.pop());
3058    let recv = vm.pop();
3059    let args = host::iter_all(&args_arr).unwrap_or_default();
3060    if let Some(r) = index_element_call(&recv, &name, &args) {
3061        return finish(vm, r);
3062    }
3063    let r = host::call_method(&recv, &name, args);
3064    finish(vm, r)
3065}
3066
3067// ── numeric hook ──────────────────────────────────────────────────────────────
3068
3069/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
3070/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
3071///
3072/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
3073/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
3074/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
3075/// rather than inside `JsHost::arith`, because calling back into JS re-enters
3076/// the VM and `arith` runs under the host's `RefCell` borrow.
3077pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
3078    use NumOp::*;
3079    let (a, b) = match op {
3080        // `==`/`!=` only convert when the OTHER side is a primitive that can be
3081        // compared numerically or textually; `{} == {}` stays a reference check.
3082        Eq | Ne => {
3083            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
3084            match (pa, pb) {
3085                (false, true) if coerces_against_object(b) => {
3086                    (host::to_primitive(a, "default")?, b.clone())
3087                }
3088                (true, false) if coerces_against_object(a) => {
3089                    (a.clone(), host::to_primitive(b, "default")?)
3090                }
3091                _ => (a.clone(), b.clone()),
3092            }
3093        }
3094        // `+` uses the default hint (`valueOf` first, but a string result still
3095        // selects concatenation); everything else uses the number hint.
3096        Add => (
3097            host::to_primitive(a, "default")?,
3098            host::to_primitive(b, "default")?,
3099        ),
3100        _ => (
3101            host::to_primitive(a, "number")?,
3102            host::to_primitive(b, "number")?,
3103        ),
3104    };
3105    reject_symbol_operand(op, &a, &b)?;
3106    with_host(|h| h.arith(op, &a, &b))
3107}
3108
3109/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
3110/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
3111/// concatenated `Symbol(desc)` into the result.
3112///
3113/// Which of the two messages V8 uses is decided by whether the operation is
3114/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
3115/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
3116/// `Symbol() + Symbol()` and `Symbol() * 1` are all
3117/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
3118/// (`Symbol() == 1` is `false`), so they are left alone.
3119fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
3120    use NumOp::*;
3121    if matches!(op, Eq | Ne) {
3122        return Ok(());
3123    }
3124    let (sym, concat) = with_host(|h| {
3125        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
3126        let is_str =
3127            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
3128        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
3129    });
3130    if !sym {
3131        return Ok(());
3132    }
3133    Err(host::type_error(if matches!(op, Add) && concat {
3134        "Cannot convert a Symbol value to a string"
3135    } else {
3136        "Cannot convert a Symbol value to a number"
3137    }))
3138}
3139
3140/// Whether a primitive `v` makes `==` against an object convert that object
3141/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
3142/// `undefined` and booleans are settled without a `ToPrimitive` call
3143/// (a boolean is coerced to a number first, and then it does).
3144fn coerces_against_object(v: &Value) -> bool {
3145    match v {
3146        Value::Undef => false,
3147        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
3148        _ => with_host(|h| !h.is_null(v)),
3149    }
3150}
3151
3152// ══ standard library ═══════════════════════════════════════════════════════════
3153
3154/// Namespaces reachable as bare globals.
3155fn is_namespace(name: &str) -> bool {
3156    matches!(
3157        name,
3158        "console"
3159            | "Math"
3160            | "JSON"
3161            | "Object"
3162            | "Array"
3163            | "Number"
3164            | "String"
3165            | "Boolean"
3166            | "Symbol"
3167            | "Reflect"
3168            | "Promise"
3169            | "process"
3170            | "Buffer"
3171            | "URL"
3172            | "URLSearchParams"
3173    )
3174}
3175
3176const GLOBAL_FUNCS: &[&str] = &[
3177    "parseInt",
3178    "parseFloat",
3179    "isNaN",
3180    "isFinite",
3181    "encodeURIComponent",
3182    "decodeURIComponent",
3183    "encodeURI",
3184    "decodeURI",
3185    // Annex B legacy encoders. Still globals on every engine, and still called
3186    // by pre-`encodeURIComponent` library code.
3187    "escape",
3188    "unescape",
3189    "eval",
3190    "String",
3191    "Number",
3192    "Boolean",
3193    "Array",
3194    "Object",
3195    "Function",
3196    "Symbol",
3197    "Map",
3198    "Set",
3199    "WeakMap",
3200    "WeakSet",
3201    "Promise",
3202    "Error",
3203    "TypeError",
3204    "RangeError",
3205    "SyntaxError",
3206    "ReferenceError",
3207    "EvalError",
3208    "URIError",
3209    "AggregateError",
3210    "BigInt",
3211    "RegExp",
3212    "Date",
3213    "ArrayBuffer",
3214    "Uint8Array",
3215    "Int8Array",
3216    "Uint8ClampedArray",
3217    "Int16Array",
3218    "Uint16Array",
3219    "Int32Array",
3220    "Uint32Array",
3221    "Float32Array",
3222    "Float64Array",
3223    "BigInt64Array",
3224    "BigUint64Array",
3225    "WeakRef",
3226    "FinalizationRegistry",
3227    "TextEncoder",
3228    "TextDecoder",
3229    // WHATWG Fetch globals (see `stdlib::fetch`).
3230    "fetch",
3231    "Headers",
3232    "Request",
3233    "Response",
3234    "Blob",
3235    "File",
3236    "FormData",
3237    "AbortController",
3238    "AbortSignal",
3239    "queueMicrotask",
3240    "setTimeout",
3241    "setInterval",
3242    "setImmediate",
3243    "clearTimeout",
3244    "clearInterval",
3245    "clearImmediate",
3246    "structuredClone",
3247    "Proxy",
3248    "require",
3249    // CommonJS loader dispatch targets referenced by per-module `require`
3250    // closures (see `module.rs`); never written by user code.
3251    "__cjs_require",
3252    "__cjs_resolve",
3253    "__cjs_cache",
3254];
3255
3256const NS_METHODS: &[&str] = &[
3257    "console.log",
3258    "console.error",
3259    "console.warn",
3260    "console.info",
3261    "console.debug",
3262    "Math.floor",
3263    "Math.ceil",
3264    "Math.round",
3265    "Math.trunc",
3266    "Math.abs",
3267    "Math.sign",
3268    "Math.max",
3269    "Math.min",
3270    "Math.pow",
3271    "Math.sqrt",
3272    "Math.cbrt",
3273    "Math.random",
3274    "Math.hypot",
3275    "Math.clz32",
3276    "Math.fround",
3277    "Math.imul",
3278    "Math.sinh",
3279    "Math.cosh",
3280    "Math.tanh",
3281    "Math.asinh",
3282    "Math.acosh",
3283    "Math.atanh",
3284    "Math.log1p",
3285    "Math.expm1",
3286    "Math.log",
3287    "Math.log2",
3288    "Math.log10",
3289    "Math.exp",
3290    "Math.sin",
3291    "Math.cos",
3292    "Math.tan",
3293    "Math.atan",
3294    "Math.atan2",
3295    "Math.asin",
3296    "Math.acos",
3297    "JSON.stringify",
3298    "JSON.parse",
3299    "Object.keys",
3300    "Object.values",
3301    "Object.entries",
3302    "Object.assign",
3303    "Object.freeze",
3304    "Object.is",
3305    "Object.fromEntries",
3306    "Object.getPrototypeOf",
3307    "Object.setPrototypeOf",
3308    "Object.create",
3309    "Object.getOwnPropertyNames",
3310    "Object.getOwnPropertySymbols",
3311    "Object.defineProperty",
3312    "Object.getOwnPropertyDescriptor",
3313    "Object.getOwnPropertyDescriptors",
3314    "Object.defineProperties",
3315    "Object.isFrozen",
3316    "Object.isSealed",
3317    "Object.seal",
3318    "Object.preventExtensions",
3319    "Object.isExtensible",
3320    "Object.hasOwn",
3321    "Object.groupBy",
3322    "Array.isArray",
3323    "Array.from",
3324    "Array.fromAsync",
3325    "Array.of",
3326    "Number.isInteger",
3327    "Number.isNaN",
3328    "Number.isFinite",
3329    "Number.isSafeInteger",
3330    "Number.parseInt",
3331    "Number.parseFloat",
3332    "String.fromCharCode",
3333    "String.fromCodePoint",
3334    "String.raw",
3335    "Symbol.for",
3336    "Symbol.keyFor",
3337    "BigInt.asIntN",
3338    "BigInt.asUintN",
3339    "Proxy.revocable",
3340    "Reflect.ownKeys",
3341    "Reflect.has",
3342    "Reflect.get",
3343    "Reflect.set",
3344    "Reflect.getPrototypeOf",
3345    "Reflect.setPrototypeOf",
3346    "Reflect.getOwnPropertyDescriptor",
3347    "Reflect.defineProperty",
3348    "Reflect.deleteProperty",
3349    "Reflect.apply",
3350    "Reflect.construct",
3351    "Reflect.isExtensible",
3352    "Reflect.preventExtensions",
3353    "Promise.resolve",
3354    "Promise.reject",
3355    "Promise.all",
3356    "Promise.allSettled",
3357    "Promise.race",
3358    "Promise.any",
3359    "Promise.withResolvers",
3360    "Map.groupBy",
3361    "Response.json",
3362    "Response.error",
3363    "Response.redirect",
3364    "AbortSignal.abort",
3365    "AbortSignal.timeout",
3366    "process.nextTick",
3367    "Error.captureStackTrace",
3368    "require.resolve",
3369];
3370
3371pub fn is_known_builtin(name: &str) -> bool {
3372    GLOBAL_FUNCS.contains(&name)
3373        || NS_METHODS.contains(&name)
3374        || is_namespace(name)
3375        || crate::stdlib::is_method(name)
3376}
3377
3378// ── dynamic functions (runtime source → callable) ────────────────────────────
3379
3380/// Build a callable from a complete function-expression source text — the ONE
3381/// dynamic-function generator on this frontend.
3382///
3383/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
3384/// wrapping parentheses needed to parse it as an expression: those are added
3385/// here, and `src` itself is retained so `Function.prototype.toString` reports
3386/// what V8 reports. The two callers synthesize different text and both shapes
3387/// are observable — see `stdlib::vm::compile_function` for the measured diff.
3388///
3389/// The body runs in the MODULE scope, never the constructing function's scope
3390/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
3391/// *global* environment). That also makes a `var` inside the body a function
3392/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
3393/// returns 6 and leaves `globalThis.zz` `undefined`.
3394pub fn dynamic_function(src: &str) -> Result<Value, String> {
3395    let f = crate::eval_in_global_scope(&format!("({src})"))?;
3396    with_host(|h| {
3397        let s = h.new_str(src.to_string());
3398        h.set_fn_prop(&f, "@@source", s);
3399    });
3400    Ok(f)
3401}
3402
3403/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
3404///
3405/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
3406/// are parameter-list fragments joined with `,` — so a fragment may itself hold
3407/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
3408/// arguments at all, both the parameter list and the body are empty.
3409///
3410/// Measured on node v26.7.0:
3411///
3412/// ```text
3413/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
3414/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
3415/// new Function('a,b','c','return [a,b,c]').length === 3
3416/// new Function('a','b','return a+b').name       === 'anonymous'
3417/// ```
3418pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
3419    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
3420    let (params, body) = match parts.split_last() {
3421        Some((body, params)) => (params.join(","), body.clone()),
3422        None => (String::new(), String::new()),
3423    };
3424    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
3425}
3426
3427/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
3428/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
3429/// other route to the same function value is an INDIRECT eval and evaluates in
3430/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
3431/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
3432/// do not.
3433///
3434/// A non-string argument is returned unchanged (19.2.1.1 step 2).
3435pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
3436    let v = arg.cloned().unwrap_or(Value::Undef);
3437    let is_string =
3438        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
3439    if !is_string {
3440        return Ok(v);
3441    }
3442    let src = with_host(|h| h.str_of(&v));
3443    let chunk = crate::load_merged(crate::compile_completion(&src)?);
3444    if direct {
3445        host::run_chunk_on(chunk)
3446    } else {
3447        host::run_chunk_in_global_scope(chunk)
3448    }
3449}
3450
3451/// Call a resolved builtin function (global or `namespace.method`).
3452pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
3453    // `require(spec)`: the ENTRY script's top-level require — core module first,
3454    // else the CommonJS loader resolving from the entry file's directory.
3455    if name == "require" {
3456        let spec = with_host(|h| h.str_of(&arg0(&args)));
3457        return crate::module::require(&spec, &crate::module::entry_dir());
3458    }
3459    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
3460    // into the loader, resolving `spec` against the module's own directory.
3461    if name == "__cjs_require" {
3462        let spec = with_host(|h| h.str_of(&arg0(&args)));
3463        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
3464        return crate::module::require(&spec, std::path::Path::new(&from));
3465    }
3466    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
3467    if name == "require.resolve" {
3468        let spec = with_host(|h| h.str_of(&arg0(&args)));
3469        if crate::stdlib::resolve(&spec).is_some() {
3470            return Ok(with_host(|h| h.new_str(spec)));
3471        }
3472        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
3473            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
3474            None => Err(crate::host::plain_coded_error(
3475                "Error",
3476                "MODULE_NOT_FOUND",
3477                &format!("Cannot find module '{spec}'"),
3478            )),
3479        };
3480    }
3481    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
3482    // path (core modules resolve to the bare specifier, as in Node).
3483    if name == "__cjs_resolve" {
3484        let spec = with_host(|h| h.str_of(&arg0(&args)));
3485        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
3486        if crate::stdlib::resolve(&spec).is_some() {
3487            return Ok(with_host(|h| h.new_str(spec)));
3488        }
3489        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
3490            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
3491            None => Err(crate::host::plain_coded_error(
3492                "Error",
3493                "MODULE_NOT_FOUND",
3494                &format!("Cannot find module '{spec}'"),
3495            )),
3496        };
3497    }
3498    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
3499    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
3500    // stack-introspection pattern used by `depd`), it is called with a synthetic
3501    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
3502    if name == "Error.captureStackTrace" {
3503        let target = arg0(&args);
3504        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
3505        let stack = match prep {
3506            Some(f)
3507                if matches!(
3508                    with_host(|h| h.get(&f).cloned()),
3509                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
3510                ) =>
3511            {
3512                let sites = crate::module::callsite_stack(10)?;
3513                host::invoke(&f, vec![target.clone(), sites], None)?
3514            }
3515            _ => with_host(|h| h.new_str("")),
3516        };
3517        let _ = set_property(&target, "stack", stack);
3518        return Ok(Value::Undef);
3519    }
3520    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
3521    if let Some(r) = crate::stdlib::call(name, &args) {
3522        return r;
3523    }
3524    match name {
3525        "console.log" | "console.info" | "console.debug" => {
3526            print_line(&args, false);
3527            Ok(Value::Undef)
3528        }
3529        "console.error" | "console.warn" => {
3530            print_line(&args, true);
3531            Ok(Value::Undef)
3532        }
3533        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args))),
3534        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args))),
3535        "isNaN" => Ok(Value::Bool(arg_num(&args, 0).is_nan())),
3536        "isFinite" => Ok(Value::Bool(arg_num(&args, 0).is_finite())),
3537        "encodeURIComponent" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), false),
3538        "encodeURI" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), true),
3539        "decodeURIComponent" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), false),
3540        "decodeURI" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), true),
3541        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
3542        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
3543        // Reaching `eval` through this table means the eval FUNCTION VALUE was
3544        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
3545        // Those are INDIRECT evals and run in the global scope. A literal
3546        // `eval(src)` is intercepted earlier, in `host::call_named`.
3547        "eval" => eval_source(args.first(), false),
3548        // `new Function(...)` and `Function(...)` are the same operation
3549        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
3550        // [[Construct]]), so both route to the one generator.
3551        "Function" => function_ctor(&args),
3552        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
3553        // (DEP0005). Node still supports it and still routes it to the same place
3554        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
3555        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
3556        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
3557        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
3558        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
3559        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
3560        // where Node warns.
3561        "Buffer" => {
3562            crate::stdlib::process::emit_deprecation_warning(
3563                "DEP0005",
3564                "Buffer() is deprecated due to security and usability issues. \
3565                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
3566                 Buffer.from() methods instead.",
3567            );
3568            crate::stdlib::construct("Buffer", &args)
3569                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
3570        }
3571        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
3572        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
3573        "Number.isNaN" => Ok(Value::Bool(
3574            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
3575        )),
3576        "Number.isFinite" => Ok(Value::Bool(
3577            matches!(arg0(&args), Value::Float(f) if f.is_finite())
3578                || matches!(arg0(&args), Value::Int(_)),
3579        )),
3580        "String" => {
3581            if args.is_empty() {
3582                Ok(with_host(|h| h.new_str("")))
3583            } else {
3584                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
3585                // is allowed); everything else via ToString method dispatch.
3586                host::string_ctor_value(&args[0])
3587            }
3588        }
3589        "Number" => Ok(Value::Float(if args.is_empty() {
3590            0.0
3591        } else {
3592            // ToNumber, which for an object runs ToPrimitive (a JS `valueOf` call).
3593            host::to_number_value(&args[0])?
3594        })),
3595        "BigInt" => bigint_ctor(&arg0(&args)),
3596        "RegExp" => regexp_ctor(&args),
3597        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
3598        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
3599        // Each argument is truncated to a uint16 and taken as one code UNIT, so
3600        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
3601        // a surrogate PAIR of arguments composes into one character.
3602        "String.fromCharCode" => Ok(with_host(|h| {
3603            let units: Vec<u16> = args
3604                .iter()
3605                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
3606                .collect();
3607            let s = crate::utf16::to_string_lossy(&units);
3608            h.new_str(s)
3609        })),
3610        // `fromCodePoint` takes whole code POINTS and rejects anything that is
3611        // not one — including a lone surrogate, which `fromCharCode` accepts.
3612        "String.fromCodePoint" => {
3613            let mut s = String::new();
3614            for a in &args {
3615                let n = with_host(|h| h.to_number(a));
3616                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
3617                {
3618                    char::from_u32(n as u32)
3619                } else {
3620                    None
3621                };
3622                match cp {
3623                    Some(c) => s.push(c),
3624                    None => {
3625                        return Err(format!(
3626                            "RangeError: Invalid code point {}",
3627                            with_host(|h| h.str_of(a))
3628                        ))
3629                    }
3630                }
3631            }
3632            Ok(new_s(s))
3633        }
3634        "String.raw" => string_raw(&args),
3635        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
3636        "Array" => construct_builtin("Array", args),
3637        "Array.of" => Ok(with_host(|h| h.new_array(args))),
3638        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
3639        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
3640        "Array.isArray" => {
3641            let v = arg0(&args);
3642            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
3643            Ok(Value::Bool(matches!(
3644                with_host(|h| h.get(&subject).cloned()),
3645                Some(JsObj::Array(_))
3646            )))
3647        }
3648        "Array.from" => array_from(args),
3649        "Array.fromAsync" => array_from_async(args),
3650        "Object" => Ok(object_call(args)),
3651        "Object.keys" => object_keys(args, 0),
3652        "Object.values" => object_keys(args, 1),
3653        "Object.entries" => object_keys(args, 2),
3654        "Object.assign" => object_assign(args),
3655        "Object.freeze" => {
3656            let v = arg0(&args);
3657            with_host(|h| h.seal_object(&v, true));
3658            Ok(v)
3659        }
3660        "Object.seal" => {
3661            let v = arg0(&args);
3662            with_host(|h| h.seal_object(&v, false));
3663            Ok(v)
3664        }
3665        "Object.preventExtensions" => {
3666            let v = arg0(&args);
3667            if crate::proxy::prevent_extensions(&v)? {
3668                return Ok(v);
3669            }
3670            with_host(|h| h.prevent_extensions(&v));
3671            Ok(v)
3672        }
3673        "Object.isFrozen" => Ok(Value::Bool(with_host(|h| h.is_sealed(&arg0(&args), true)))),
3674        "Object.isSealed" => Ok(Value::Bool(with_host(|h| h.is_sealed(&arg0(&args), false)))),
3675        "Object.isExtensible" => {
3676            let v = arg0(&args);
3677            match crate::proxy::is_extensible(&v)? {
3678                Some(b) => Ok(Value::Bool(b)),
3679                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
3680            }
3681        }
3682        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
3683        // distinct from -0.
3684        "Object.is" => {
3685            let a = arg0(&args);
3686            let b = args.get(1).cloned().unwrap_or(Value::Undef);
3687            let num = |v: &Value| match v {
3688                Value::Int(n) => Some(*n as f64),
3689                Value::Float(f) => Some(*f),
3690                _ => None,
3691            };
3692            let r = match (num(&a), num(&b)) {
3693                (Some(x), Some(y)) => {
3694                    if x.is_nan() && y.is_nan() {
3695                        true
3696                    } else if x == 0.0 && y == 0.0 {
3697                        x.is_sign_negative() == y.is_sign_negative()
3698                    } else {
3699                        x == y
3700                    }
3701                }
3702                _ => with_host(|h| h.strict_eq(&a, &b)),
3703            };
3704            Ok(Value::Bool(r))
3705        }
3706        "Object.fromEntries" => object_from_entries(args),
3707        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
3708        // so the proxy form cannot share `prototype_of`'s infallible signature.
3709        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
3710            let v = arg0(&args);
3711            match crate::proxy::get_prototype_of(&v)? {
3712                Some(p) => Ok(p),
3713                None => Ok(prototype_of(&v)),
3714            }
3715        }
3716        "Object.setPrototypeOf" => {
3717            let obj = arg0(&args);
3718            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
3719            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
3720                reject_bad_prototype(&proto)?;
3721                crate::proxy::set_prototype_of(&obj, &proto)?;
3722                return Ok(obj);
3723            }
3724            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
3725            // prototype type check, then — only for an actual object target —
3726            // the extensibility check. A PRIMITIVE target is returned untouched
3727            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
3728            // extensibility test cannot come first.
3729            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
3730                return Err(host::type_error(
3731                    "Object.setPrototypeOf called on null or undefined",
3732                ));
3733            }
3734            reject_bad_prototype(&proto)?;
3735            if with_host(|h| is_object_like(h, &obj)) {
3736                // Setting the SAME prototype is a no-op and stays legal even on a
3737                // frozen object: node v26.7.0 accepts
3738                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
3739                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
3740                // link still has `Object.prototype`, and comparing against the
3741                // absent link would call that a change.
3742                let cur = prototype_of(&obj);
3743                let same = with_host(|h| h.strict_eq(&cur, &proto));
3744                if !same && !with_host(|h| h.is_extensible(&obj)) {
3745                    return Err(host::type_error("#<Object> is not extensible"));
3746                }
3747                with_host(|h| h.set_proto(&obj, proto));
3748            }
3749            Ok(obj)
3750        }
3751        "Object.create" => object_create(args),
3752        "Object.getOwnPropertyNames" => object_keys(args, 3),
3753        "Object.getOwnPropertySymbols" => {
3754            let v = arg0(&args);
3755            require_object_coercible(&v)?;
3756            let syms = proxy_or_own_symbol_keys(&v)?;
3757            Ok(with_host(|h| h.new_array(syms)))
3758        }
3759        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
3760        "Object.hasOwn" => {
3761            let obj = arg0(&args);
3762            let key = args.get(1).cloned().unwrap_or(Value::Undef);
3763            object_builtin_method(&obj, "hasOwnProperty", vec![key])
3764        }
3765        "Object.defineProperty" => object_define_property(args),
3766        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
3767        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
3768        "Object.defineProperties" => object_define_properties(args),
3769        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
3770        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
3771        "Object.groupBy" => object_group_by(args),
3772        "Symbol" => Ok(with_host(|h| {
3773            let desc = args
3774                .first()
3775                .filter(|a| !matches!(a, Value::Undef))
3776                .map(|a| h.str_of(a));
3777            h.new_symbol(desc)
3778        })),
3779        "Symbol.for" => Ok(with_host(|h| {
3780            let key = h.str_of(&arg0(&args));
3781            h.symbol_for(&key)
3782        })),
3783        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
3784        // description read: it answers only for symbols `Symbol.for` created.
3785        // Returning the description made every symbol look registered —
3786        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
3787        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
3788        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
3789        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
3790        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
3791        "Proxy.revocable" => crate::proxy::revocable(&args),
3792        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
3793        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
3794        // own properties, so there is no second half to append).
3795        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
3796        // non-enumerable included, strings first and then the SYMBOLS.
3797        "Reflect.ownKeys" => {
3798            let v = arg0(&args);
3799            let names = object_keys(args, 3)?;
3800            let syms = proxy_or_own_symbol_keys(&v)?;
3801            if syms.is_empty() {
3802                return Ok(names);
3803            }
3804            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
3805            all.extend(syms);
3806            Ok(with_host(|h| h.new_array(all)))
3807        }
3808        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
3809        "Reflect.defineProperty" => {
3810            object_define_property(args)?;
3811            Ok(Value::Bool(true))
3812        }
3813        "Reflect.deleteProperty" => {
3814            let obj = arg0(&args);
3815            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3816            Ok(Value::Bool(delete_property(&obj, &k)?))
3817        }
3818        "Reflect.setPrototypeOf" => {
3819            let obj = arg0(&args);
3820            let p = args.get(1).cloned().unwrap_or(Value::Undef);
3821            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
3822                crate::proxy::set_prototype_of(&obj, &p)?;
3823                return Ok(Value::Bool(true));
3824            }
3825            with_host(|h| h.set_proto(&obj, p));
3826            Ok(Value::Bool(true))
3827        }
3828        "Reflect.isExtensible" => {
3829            let v = arg0(&args);
3830            match crate::proxy::is_extensible(&v)? {
3831                Some(b) => Ok(Value::Bool(b)),
3832                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
3833            }
3834        }
3835        "Reflect.preventExtensions" => {
3836            let v = arg0(&args);
3837            if crate::proxy::prevent_extensions(&v)? {
3838                return Ok(Value::Bool(true));
3839            }
3840            with_host(|h| h.prevent_extensions(&v));
3841            Ok(Value::Bool(true))
3842        }
3843        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
3844        "Reflect.apply" => {
3845            let f = arg0(&args);
3846            let this = args.get(1).cloned();
3847            let list = with_host(|h| h.iter_vec(&args.get(2).cloned().unwrap_or(Value::Undef)))
3848                .unwrap_or_default();
3849            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
3850        }
3851        "Reflect.construct" => {
3852            let f = arg0(&args);
3853            let list = with_host(|h| h.iter_vec(&args.get(1).cloned().unwrap_or(Value::Undef)))
3854                .unwrap_or_default();
3855            host::construct(&f, list)
3856        }
3857        "Reflect.has" => {
3858            let obj = arg0(&args);
3859            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3860            Ok(Value::Bool(has_property(&obj, &k)?))
3861        }
3862        // `Reflect.get(target, key, receiver)` — the optional third argument is
3863        // what a getter sees as `this` (28.1.6). Defaults to the target.
3864        "Reflect.get" => {
3865            let obj = arg0(&args);
3866            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3867            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
3868            get_property_recv(&obj, &k, &receiver)
3869        }
3870        "Reflect.set" => {
3871            let obj = arg0(&args);
3872            let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
3873            let v = args.get(2).cloned().unwrap_or(Value::Undef);
3874            let _ = set_property(&obj, &k, v);
3875            Ok(Value::Bool(true))
3876        }
3877        "JSON.stringify" => json_stringify(args),
3878        "JSON.parse" => json_parse(args),
3879        "structuredClone" => Ok(deep_clone(&arg0(&args))),
3880        "fetch" => crate::stdlib::fetch::fetch(&args),
3881        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
3882        // suffix is the signal's heap index.
3883        _ if name.starts_with("@@aborttimeout:") => {
3884            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
3885            crate::stdlib::fetch::fire_timeout_abort(idx)
3886        }
3887        // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
3888        // implementation. Nothing here waits on backpressure, so it only has to
3889        // BE callable — an implementation that ends with `cb()`, which the
3890        // stream contract requires, would otherwise throw.
3891        "@@streamWriteCallback" => Ok(Value::Undef),
3892        "queueMicrotask" | "process.nextTick" => {
3893            let cb = arg0(&args);
3894            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
3895            enqueue_microtask(name == "process.nextTick", cb, rest);
3896            Ok(Value::Undef)
3897        }
3898        "setTimeout" | "setInterval" | "setImmediate" => Ok(schedule_timer(name, args)),
3899        "clearTimeout" | "clearInterval" | "clearImmediate" => {
3900            clear_timer(&arg0(&args));
3901            Ok(Value::Undef)
3902        }
3903        "Promise.resolve" => promise_resolve(arg0(&args)),
3904        "Promise.reject" => promise_reject(arg0(&args)),
3905        "Promise.all" => promise_all(args, AllMode::All),
3906        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
3907        "Promise.race" => promise_race(args, false),
3908        "Promise.any" => promise_race(args, true),
3909        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
3910        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
3911        "Promise.withResolvers" => promise_with_resolvers(),
3912        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
3913        // `cb(item, i)` result (SameValueZero), each value an array of members.
3914        "Map.groupBy" => map_group_by(args),
3915        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
3916        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
3917        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
3918        _ if name.starts_with("@@presolve:") => {
3919            let id: u32 = name[11..].parse().unwrap_or(0);
3920            host::resolve_promise_val(id, arg0(&args));
3921            Ok(Value::Undef)
3922        }
3923        _ if name.starts_with("@@preject:") => {
3924            let id: u32 = name[10..].parse().unwrap_or(0);
3925            host::reject_promise_val(id, arg0(&args));
3926            Ok(Value::Undef)
3927        }
3928        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
3929        // index so calling it twice is the spec's no-op rather than a re-tear.
3930        _ if name.starts_with("@@prevoke:") => {
3931            let i: u32 = name[10..].parse().unwrap_or(0);
3932            Ok(crate::proxy::revoke(i))
3933        }
3934        _ if name.starts_with("@@finpass:") => {
3935            // finally(cb) on fulfill: run cb, then pass the value through.
3936            let i: u32 = name[10..].parse().unwrap_or(0);
3937            let cb = Value::Obj(i);
3938            host::invoke(&cb, Vec::new(), None)?;
3939            Ok(arg0(&args))
3940        }
3941        _ if name.starts_with("@@finthrow:") => {
3942            // finally(cb) on reject: run cb, then re-throw the reason.
3943            let i: u32 = name[11..].parse().unwrap_or(0);
3944            let cb = Value::Obj(i);
3945            host::invoke(&cb, Vec::new(), None)?;
3946            let reason = arg0(&args);
3947            with_host(|h| h.exc = Some(reason.clone()));
3948            Err(with_host(|h| error_string(h, &reason)))
3949        }
3950        _ => Err(host::type_error(&format!("{name} is not a function"))),
3951    }
3952}
3953
3954/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
3955/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
3956/// (matching Node's messages).
3957/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
3958/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
3959/// BigInt`. The old text said "value" literally, for every input.
3960fn bigint_convert_error(v: &Value) -> String {
3961    let shown = with_host(|h| h.str_of(v));
3962    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
3963}
3964
3965fn bigint_ctor(v: &Value) -> Result<Value, String> {
3966    use num_bigint::BigInt;
3967    let big = match v {
3968        Value::Bool(b) => BigInt::from(*b as i64),
3969        Value::Int(n) => BigInt::from(*n),
3970        Value::Float(f) => {
3971            if !f.is_finite() || f.fract() != 0.0 {
3972                let disp = with_host(|h| h.str_of(v));
3973                return Err(format!(
3974                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
3975                ));
3976            }
3977            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
3978            // .toString` switches to exponential notation at 1e21, and
3979            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
3980            // threw `Cannot convert value to a BigInt` where node returns
3981            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
3982            // value, which is also what node reports for a magnitude past the
3983            // exactly-representable range (`BigInt(1e30)` is
3984            // `1000000000000000019884624838656n` in both).
3985            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
3986                Some(b) => b,
3987                None => return Err(bigint_convert_error(v)),
3988            }
3989        }
3990        Value::Str(s) => match host::parse_bigint_str(s) {
3991            Some(b) => b,
3992            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
3993        },
3994        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
3995            Some(JsObj::BigInt(b)) => b,
3996            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
3997                Some(b) => b,
3998                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
3999            },
4000            _ => return Err(bigint_convert_error(v)),
4001        },
4002        _ => return Err(bigint_convert_error(v)),
4003    };
4004    Ok(with_host(|h| h.new_bigint(big)))
4005}
4006
4007/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
4008/// its source (and flags, unless new ones are given).
4009fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
4010    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
4011        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
4012        _ => {
4013            let a0 = arg0(args);
4014            let src = if matches!(a0, Value::Undef) {
4015                String::new()
4016            } else {
4017                with_host(|h| h.str_of(&a0))
4018            };
4019            (src, None)
4020        }
4021    };
4022    let flags = match args.get(1) {
4023        Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
4024        _ => existing_flags.unwrap_or_default(),
4025    };
4026    // An empty source compiles as the JS canonical `(?:)`.
4027    let src = if source.is_empty() {
4028        "(?:)".to_string()
4029    } else {
4030        source
4031    };
4032    crate::regexp::build_regexp(&src, &flags)
4033}
4034
4035/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
4036/// two's-complement (signed) or unsigned integer.
4037fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
4038    use num_bigint::BigInt;
4039    use num_traits::Signed;
4040    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
4041    if bits < 0 {
4042        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
4043    }
4044    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
4045        Some(b) => b,
4046        None => return Err(host::type_error("Cannot convert to a BigInt")),
4047    };
4048    let bits = bits as u32;
4049    if bits == 0 {
4050        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
4051    }
4052    let modulus = BigInt::from(1) << bits; // 2^bits
4053                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
4054    let mut r = &x % &modulus;
4055    if r.is_negative() {
4056        r += &modulus;
4057    }
4058    if !unsigned {
4059        let half = BigInt::from(1) << (bits - 1);
4060        if r >= half {
4061            r -= &modulus;
4062        }
4063    }
4064    Ok(with_host(|h| h.new_bigint(r)))
4065}
4066
4067/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
4068/// interleaved with the substitutions.
4069fn string_raw(args: &[Value]) -> Result<Value, String> {
4070    let call_site = arg0(args);
4071    let raw = get_property(&call_site, "raw")?;
4072    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
4073    let mut out = String::new();
4074    for (i, r) in raws.iter().enumerate() {
4075        out.push_str(&with_host(|h| h.str_of(r)));
4076        if i + 1 < raws.len() {
4077            if let Some(sub) = args.get(i + 1) {
4078                out.push_str(&with_host(|h| h.str_of(sub)));
4079            }
4080        }
4081    }
4082    Ok(with_host(|h| h.new_str(out)))
4083}
4084
4085/// `Object(x)`: box/pass-through — for our model, non-object args just return a
4086/// fresh object; objects pass through.
4087fn object_call(args: Vec<Value>) -> Value {
4088    let a = arg0(&args);
4089    if matches!(
4090        with_host(|h| h.get(&a).cloned()),
4091        Some(JsObj::Object(_)) | Some(JsObj::Array(_))
4092    ) {
4093        a
4094    } else {
4095        with_host(|h| h.new_object(IndexMap::new()))
4096    }
4097}
4098
4099/// Construct via `new` for the builtin constructors.
4100pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
4101    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
4102    if let Some(r) = crate::stdlib::construct(name, &args) {
4103        return r;
4104    }
4105    match name {
4106        "Array" => {
4107            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
4108            // A single NUMBER argument is a length and is validated as one
4109            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
4110            // `new Array(2**32)` are all `RangeError: Invalid array length` on
4111            // node v26.7.0; only a non-number single argument is an element.
4112            if args.len() == 1 {
4113                if let Value::Float(_) | Value::Int(_) = args[0] {
4114                    let n = host::to_array_length(&args[0])?;
4115                    // Every element of `new Array(n)` is a HOLE, not a stored
4116                    // `undefined`: `Object.keys(Array(3))` is `[]`.
4117                    return Ok(with_host(|h| {
4118                        let a = h.new_array(vec![Value::Undef; n]);
4119                        h.mark_hole_range(&a, 0..n);
4120                        a
4121                    }));
4122                }
4123            }
4124            Ok(with_host(|h| h.new_array(args)))
4125        }
4126        "Object" => Ok(object_call(args)),
4127        "Map" | "WeakMap" => {
4128            let weak = name == "WeakMap";
4129            let m = with_host(|h| {
4130                h.alloc(JsObj::Map {
4131                    entries: indexmap::IndexMap::new(),
4132                    weak,
4133                })
4134            });
4135            if let Some(init) = args
4136                .first()
4137                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
4138            {
4139                let pairs = host::iter_all(init)?;
4140                for p in pairs {
4141                    let kv = host::iter_all(&p)?;
4142                    let k = kv.first().cloned().unwrap_or(Value::Undef);
4143                    let v = kv.get(1).cloned().unwrap_or(Value::Undef);
4144                    map_method(&m, "set", vec![k, v])?;
4145                }
4146            }
4147            Ok(m)
4148        }
4149        "Set" | "WeakSet" => {
4150            let weak = name == "WeakSet";
4151            let s = with_host(|h| {
4152                h.alloc(JsObj::Set {
4153                    entries: indexmap::IndexMap::new(),
4154                    weak,
4155                })
4156            });
4157            if let Some(init) = args
4158                .first()
4159                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
4160            {
4161                let vals = host::iter_all(init)?;
4162                for v in vals {
4163                    set_method(&s, "add", vec![v])?;
4164                }
4165            }
4166            Ok(s)
4167        }
4168        "Promise" => new_promise(arg0(&args)),
4169        "Proxy" => crate::proxy::create(&args),
4170        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
4171        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
4172        // deprecation wrapper this way, so `require('body-parser')` — and with it
4173        // `require('express')` — dies at load without it.
4174        "Function" => function_ctor(&args),
4175        "RegExp" => regexp_ctor(&args),
4176        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
4177        "Error" => Ok(make_error(name, &args)),
4178        n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
4179        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
4180    }
4181}
4182
4183fn make_error(name: &str, args: &[Value]) -> Value {
4184    // `new AggregateError(errors, message)` takes the causes FIRST; every other
4185    // error constructor takes the message first.
4186    let agg = name == "AggregateError";
4187    let (errors, args) = if agg {
4188        (
4189            Some(args.first().cloned().unwrap_or(Value::Undef)),
4190            args.get(1..).unwrap_or(&[]),
4191        )
4192    } else {
4193        (None, args)
4194    };
4195    with_host(|h| {
4196        h.ensure_error_protos();
4197        let mut props: IndexMap<String, Value> = IndexMap::new();
4198        let msg = args
4199            .first()
4200            .filter(|a| !matches!(a, Value::Undef))
4201            .map(|a| h.str_of(a));
4202        if let Some(m) = &msg {
4203            let mv = h.new_str(m.clone());
4204            props.insert("message".into(), mv);
4205        }
4206        // `.stack` is engine-specific; a simple `Name: message` header line
4207        // suffices for parity (the fuzzer never prints raw stacks).
4208        let frames = h.stack_frames();
4209        let stack = match &msg {
4210            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
4211            _ => format!("{name}{frames}"),
4212        };
4213        let sv = h.new_str(stack);
4214        props.insert("stack".into(), sv);
4215        if let Some(errs) = errors {
4216            // Materialize the iterable into the own `errors` array property.
4217            let items = h.iter_vec(&errs).unwrap_or_default();
4218            let arr = h.new_array(items);
4219            props.insert("errors".into(), arr);
4220        }
4221        // `new Error(msg, { cause })` (ES2022): installed only when the options
4222        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
4223        let opts = args.get(1);
4224        if let Some(cause) = opts.and_then(|o| match h.get(o) {
4225            Some(JsObj::Object(p)) => p.get("cause").cloned(),
4226            _ => None,
4227        }) {
4228            props.insert("cause".into(), cause);
4229        }
4230        let e = h.new_object(props);
4231        if let Some(p) = host::error_proto_of(h, name) {
4232            h.set_proto(&e, p);
4233        }
4234        // Every own slot an error constructor installs is non-enumerable in V8,
4235        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
4236        // `{}` — properties a *script* later assigns stay enumerable.
4237        for k in ["message", "stack", "errors", "cause"] {
4238            h.hide_prop(&e, k);
4239        }
4240        e
4241    })
4242}
4243
4244fn print_line(args: &[Value], stderr: bool) {
4245    // Node's console.log(...args) === util.format(...args): printf-style
4246    // substitution when the first arg is a format string, else inspect-and-join.
4247    let line: String = crate::stdlib::util::format(args);
4248    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
4249}
4250
4251fn arg0(args: &[Value]) -> Value {
4252    args.first().cloned().unwrap_or(Value::Undef)
4253}
4254fn arg_num(args: &[Value], i: usize) -> f64 {
4255    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
4256}
4257
4258fn is_integer(v: Value) -> bool {
4259    match v {
4260        Value::Int(_) => true,
4261        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
4262        _ => false,
4263    }
4264}
4265fn is_safe_integer(v: Value) -> bool {
4266    match v {
4267        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
4268        Value::Int(_) => true,
4269        _ => false,
4270    }
4271}
4272
4273/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
4274/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
4275/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
4276fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
4277    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
4278    const UNRESERVED: &[u8] =
4279        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
4280    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
4281    const RESERVED: &[u8] = b";,/?:@&=+$#";
4282    let mut out = String::with_capacity(s.len());
4283    for &b in s.as_bytes() {
4284        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
4285            out.push(b as char);
4286        } else {
4287            out.push('%');
4288            out.push(
4289                char::from_digit((b >> 4) as u32, 16)
4290                    .unwrap()
4291                    .to_ascii_uppercase(),
4292            );
4293            out.push(
4294                char::from_digit((b & 0xf) as u32, 16)
4295                    .unwrap()
4296                    .to_ascii_uppercase(),
4297            );
4298        }
4299    }
4300    Ok(with_host(|h| h.new_str(out)))
4301}
4302
4303/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
4304/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
4305/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
4306fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
4307    const RESERVED: &[u8] = b";,/?:@&=+$#";
4308    let bytes = s.as_bytes();
4309    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
4310    let mut i = 0;
4311    while i < bytes.len() {
4312        if bytes[i] == b'%' {
4313            if i + 2 >= bytes.len() {
4314                return Err("URIError: URI malformed".into());
4315            }
4316            let hi = (bytes[i + 1] as char).to_digit(16);
4317            let lo = (bytes[i + 2] as char).to_digit(16);
4318            match (hi, lo) {
4319                (Some(h), Some(l)) => {
4320                    let byte = (h * 16 + l) as u8;
4321                    // decodeURI keeps reserved-delimiter escapes literal.
4322                    if uri && RESERVED.contains(&byte) {
4323                        out.extend_from_slice(&bytes[i..i + 3]);
4324                    } else {
4325                        out.push(byte);
4326                    }
4327                    i += 3;
4328                }
4329                _ => return Err("URIError: URI malformed".into()),
4330            }
4331        } else {
4332            out.push(bytes[i]);
4333            i += 1;
4334        }
4335    }
4336    match String::from_utf8(out) {
4337        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
4338        Err(_) => Err("URIError: URI malformed".into()),
4339    }
4340}
4341
4342/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
4343/// present in every engine and still reached by old libraries (jQuery's cookie
4344/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
4345/// bytes, which is what separates it from `encodeURIComponent`: a unit below
4346/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
4347/// character yields the two escapes of its surrogate pair
4348/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
4349///
4350/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
4351/// it keeps `@*_+-./` and drops `!~'()`.
4352fn legacy_escape(s: &str) -> Result<Value, String> {
4353    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
4354    let mut out = String::with_capacity(s.len());
4355    for u in s.encode_utf16() {
4356        if u < 0x100 {
4357            if KEEP.contains(&(u as u8)) {
4358                out.push(u as u8 as char);
4359            } else {
4360                out.push_str(&format!("%{u:02X}"));
4361            }
4362        } else {
4363            out.push_str(&format!("%u{u:04X}"));
4364        }
4365    }
4366    Ok(with_host(|h| h.new_str(out)))
4367}
4368
4369/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
4370/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
4371/// `%XX` or `%uXXXX` escape is passed through literally
4372/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
4373///
4374/// Decoding is done in code-unit space and re-joined at the end so a
4375/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
4376fn legacy_unescape(s: &str) -> Result<Value, String> {
4377    let b = s.as_bytes();
4378    let hex = |i: usize, n: usize| -> Option<u16> {
4379        if i + n > b.len() {
4380            return None;
4381        }
4382        let mut v: u16 = 0;
4383        for &c in &b[i..i + n] {
4384            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
4385        }
4386        Some(v)
4387    };
4388    let units: Vec<u16> = s.encode_utf16().collect();
4389    let mut out: Vec<u16> = Vec::with_capacity(units.len());
4390    let mut i = 0;
4391    while i < b.len() {
4392        // Escapes are pure ASCII, so a byte index is a unit index up to here —
4393        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
4394        if b[i] == b'%' {
4395            if let Some(u) = hex(i + 1, 2) {
4396                out.push(u);
4397                i += 3;
4398                continue;
4399            }
4400            if b.get(i + 1) == Some(&b'u') {
4401                if let Some(u) = hex(i + 2, 4) {
4402                    out.push(u);
4403                    i += 6;
4404                    continue;
4405                }
4406            }
4407        }
4408        let c = s[i..].chars().next().unwrap_or('%');
4409        let mut buf = [0u16; 2];
4410        out.extend_from_slice(c.encode_utf16(&mut buf));
4411        i += c.len_utf8();
4412    }
4413    Ok(with_host(|h| {
4414        h.new_str(crate::utf16::to_string_lossy(&out))
4415    }))
4416}
4417
4418fn parse_int(args: &[Value]) -> f64 {
4419    let s = with_host(|h| h.str_of(&arg0(args)));
4420    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
4421    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
4422    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
4423    let radix_arg = args
4424        .get(1)
4425        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
4426    let radix = match radix_arg {
4427        Some(0) | None => None,
4428        Some(r) if (2..=36).contains(&r) => Some(r as u32),
4429        Some(_) => return f64::NAN,
4430    };
4431    let t = crate::utf16::js_trim_start(&s);
4432    let (neg, digits) = match t.strip_prefix('-') {
4433        Some(rest) => (true, rest),
4434        None => (false, t.strip_prefix('+').unwrap_or(t)),
4435    };
4436    let (radix, digits) = match radix {
4437        Some(16) => (
4438            16u32,
4439            digits
4440                .strip_prefix("0x")
4441                .or_else(|| digits.strip_prefix("0X"))
4442                .unwrap_or(digits),
4443        ),
4444        Some(r) => (r, digits),
4445        None => {
4446            if let Some(hex) = digits
4447                .strip_prefix("0x")
4448                .or_else(|| digits.strip_prefix("0X"))
4449            {
4450                (16, hex)
4451            } else {
4452                (10, digits)
4453            }
4454        }
4455    };
4456    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
4457    if valid.is_empty() {
4458        return f64::NAN;
4459    }
4460    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
4461    // digits and the error was mapped to `NaN`, so
4462    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
4463    // asks for the mathematical value rounded to a Number, which is what
4464    // repeated multiply-accumulate in `f64` produces.
4465    let n = if radix == 10 {
4466        // Rust's decimal float parser is correctly rounded; digit-by-digit
4467        // multiply-accumulate is not, and drifted a ULP on long inputs
4468        // (`parseInt("999999999999999999999999")` came out
4469        // 1.0000000000000003e+24 rather than 1e+24).
4470        valid.parse::<f64>().unwrap_or(f64::NAN)
4471    } else {
4472        let mut n = 0.0f64;
4473        for c in valid.chars() {
4474            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
4475        }
4476        n
4477    };
4478    if neg {
4479        -n
4480    } else {
4481        n
4482    }
4483}
4484
4485fn parse_float(args: &[Value]) -> f64 {
4486    let s = with_host(|h| h.str_of(&arg0(args)));
4487    let t = crate::utf16::js_trim_start(&s);
4488    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
4489    let inf_body = t
4490        .strip_prefix('+')
4491        .or_else(|| t.strip_prefix('-'))
4492        .unwrap_or(t);
4493    if inf_body.starts_with("Infinity") {
4494        return if t.starts_with('-') {
4495            f64::NEG_INFINITY
4496        } else {
4497            f64::INFINITY
4498        };
4499    }
4500    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
4501    // not the same as the longest run of characters that could appear in one:
4502    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
4503    // only valid once a digit follows `e`. Tracking `end` at every character
4504    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
4505    // came back NaN.
4506    let mut end = 0;
4507    let bytes = t.as_bytes();
4508    let mut seen_dot = false;
4509    let mut seen_e = false;
4510    let mut digits_before_dot = false;
4511    for (i, &c) in bytes.iter().enumerate() {
4512        match c {
4513            b'0'..=b'9' => {
4514                if !seen_dot && !seen_e {
4515                    digits_before_dot = true;
4516                }
4517                end = i + 1;
4518            }
4519            // A sign is only meaningful leading, or straight after the exponent
4520            // marker; it never completes a literal on its own.
4521            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
4522            // `1.` is a complete literal; a bare `.` is not.
4523            b'.' if !seen_dot && !seen_e => {
4524                seen_dot = true;
4525                if digits_before_dot {
4526                    end = i + 1;
4527                }
4528            }
4529            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
4530            _ => break,
4531        }
4532    }
4533    if end == 0 {
4534        return f64::NAN;
4535    }
4536    t[..end].parse::<f64>().unwrap_or(f64::NAN)
4537}
4538
4539/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
4540/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
4541/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
4542/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
4543/// rather than 1.
4544pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
4545    if exp == 0.0 {
4546        return 1.0;
4547    }
4548    if base.is_nan() || exp.is_nan() {
4549        return f64::NAN;
4550    }
4551    if base.abs() == 1.0 && exp.is_infinite() {
4552        return f64::NAN;
4553    }
4554    base.powf(exp)
4555}
4556
4557fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
4558    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
4559    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
4560    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
4561    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
4562    // exception: it never reads an argument, so `Math.random(1n)` is fine.
4563    if fname != "random"
4564        && args
4565            .iter()
4566            .any(|a| with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))))
4567    {
4568        return Err(host::type_error(
4569            "Cannot convert a BigInt value to a number",
4570        ));
4571    }
4572    let x = arg_num(args, 0);
4573    let r = match fname {
4574        "floor" => x.floor(),
4575        "ceil" => x.ceil(),
4576        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
4577        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
4578        // before the floor sees it, so it answers 1 for the largest double below
4579        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
4580        // perturbs integers above 2^52, where `x + 0.5` is no longer
4581        // representable (`Math.round(4503599627370497)` must be the input).
4582        // Splitting the zero-band cases out first also carries the signed zero
4583        // the spec asks for without a post-hoc patch.
4584        "round" => {
4585            if !x.is_finite() || x == 0.0 {
4586                x
4587            } else if x > 0.0 && x < 0.5 {
4588                0.0
4589            } else if (-0.5..0.0).contains(&x) {
4590                -0.0
4591            } else {
4592                // |x| >= 0.5, so `floor` and the subtraction are both exact
4593                // (every double >= 2^52 is already an integer and yields 0 here).
4594                let f = x.floor();
4595                if x - f >= 0.5 {
4596                    f + 1.0
4597                } else {
4598                    f
4599                }
4600            }
4601        }
4602        "trunc" => x.trunc(),
4603        "abs" => x.abs(),
4604        "sign" => {
4605            if x.is_nan() {
4606                f64::NAN
4607            } else if x > 0.0 {
4608                1.0
4609            } else if x < 0.0 {
4610                -1.0
4611            } else {
4612                x
4613            }
4614        }
4615        "sqrt" => x.sqrt(),
4616        "cbrt" => x.cbrt(),
4617        "exp" => x.exp(),
4618        "log" => x.ln(),
4619        "log2" => x.log2(),
4620        "log10" => x.log10(),
4621        "sin" => x.sin(),
4622        "cos" => x.cos(),
4623        "tan" => x.tan(),
4624        "asin" => x.asin(),
4625        "acos" => x.acos(),
4626        "atan" => x.atan(),
4627        "atan2" => x.atan2(arg_num(args, 1)),
4628        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
4629        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
4630        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
4631        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
4632        // clause is shared.
4633        "pow" => js_pow(x, arg_num(args, 1)),
4634        // Hyperbolics and the two precision-preserving log/exp forms.
4635        "sinh" => x.sinh(),
4636        "cosh" => x.cosh(),
4637        "tanh" => x.tanh(),
4638        "asinh" => x.asinh(),
4639        "acosh" => x.acosh(),
4640        "atanh" => x.atanh(),
4641        "log1p" => x.ln_1p(),
4642        "expm1" => x.exp_m1(),
4643        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
4644        // wraparound, reinterpret as a signed 32-bit result.
4645        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
4646        "hypot" => {
4647            // Scale by the largest magnitude before squaring — this avoids the
4648            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
4649            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
4650            let mut max = 0.0f64;
4651            for x in &xs {
4652                if x.abs() > max {
4653                    max = x.abs();
4654                }
4655            }
4656            if xs.iter().any(|x| x.is_infinite()) {
4657                f64::INFINITY
4658            } else if max == 0.0 || !max.is_finite() {
4659                max
4660            } else {
4661                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
4662                max * s.sqrt()
4663            }
4664        }
4665        "random" => pseudo_random(),
4666        "max" => {
4667            if args.is_empty() {
4668                f64::NEG_INFINITY
4669            } else {
4670                let mut m = f64::NEG_INFINITY;
4671                for a in args {
4672                    let n = with_host(|h| h.to_number(a));
4673                    if n.is_nan() {
4674                        return Ok(Value::Float(f64::NAN));
4675                    }
4676                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
4677                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
4678                    // must not keep the -0 the first iteration installed.
4679                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
4680                        m = n;
4681                    }
4682                }
4683                m
4684            }
4685        }
4686        "min" => {
4687            if args.is_empty() {
4688                f64::INFINITY
4689            } else {
4690                let mut m = f64::INFINITY;
4691                for a in args {
4692                    let n = with_host(|h| h.to_number(a));
4693                    if n.is_nan() {
4694                        return Ok(Value::Float(f64::NAN));
4695                    }
4696                    // Mirror of `max`: -0 ranks below +0 even though `<` says
4697                    // they are equal, so `Math.min(0, -0)` is -0.
4698                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
4699                        m = n;
4700                    }
4701                }
4702                m
4703            }
4704        }
4705        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
4706        "clz32" => {
4707            let u = if x.is_finite() {
4708                x.trunc().rem_euclid(4294967296.0) as u32
4709            } else {
4710                0
4711            };
4712            u.leading_zeros() as f64
4713        }
4714        // Round to the nearest single-precision float.
4715        "fround" => (x as f32) as f64,
4716        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
4717    };
4718    Ok(Value::Float(r))
4719}
4720
4721/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
4722/// Node by nature; kept simple).
4723fn pseudo_random() -> f64 {
4724    use std::cell::Cell;
4725    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
4726    SEED.with(|s| {
4727        let mut x = s.get();
4728        x ^= x << 13;
4729        x ^= x >> 7;
4730        x ^= x << 17;
4731        s.set(x);
4732        (x >> 11) as f64 / (1u64 << 53) as f64
4733    })
4734}
4735
4736// ── Object.* ──────────────────────────────────────────────────────────────────
4737
4738fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
4739    let v = arg0(&args);
4740    require_object_coercible(&v)?;
4741    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
4742    // reports every own STRING key the trap named; the enumerating modes
4743    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
4744    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
4745        if mode == 3 {
4746            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
4747            return Ok(with_host(|h| {
4748                let out: Vec<Value> = keys
4749                    .into_iter()
4750                    .filter(|k| !host::is_symbol_key(k))
4751                    .map(|k| h.new_str(k))
4752                    .collect();
4753                h.new_array(out)
4754            }));
4755        }
4756        let entries = crate::proxy::own_enum_entries(&v)?;
4757        return Ok(with_host(|h| {
4758            let out: Vec<Value> = entries
4759                .into_iter()
4760                .map(|(k, val)| match mode {
4761                    0 => h.new_str(k),
4762                    1 => val,
4763                    _ => {
4764                        let ks = h.new_str(k);
4765                        h.new_array(vec![ks, val])
4766                    }
4767                })
4768                .collect();
4769            h.new_array(out)
4770        }));
4771    }
4772    // A builtin prototype namespace that exposes enumerable methods for copying
4773    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
4774    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&v).cloned()) {
4775        if let Some(names) = builtin_proto_method_names(&ns) {
4776            return Ok(with_host(|h| {
4777                let out: Vec<Value> = names
4778                    .iter()
4779                    .map(|name| match mode {
4780                        1 => h.alloc(JsObj::Builtin(format!(
4781                            "@proto:{}:{name}",
4782                            ns.trim_end_matches(".prototype")
4783                        ))),
4784                        2 => {
4785                            let ks = h.new_str(*name);
4786                            let val = h.alloc(JsObj::Builtin(format!(
4787                                "@proto:{}:{name}",
4788                                ns.trim_end_matches(".prototype")
4789                            )));
4790                            h.new_array(vec![ks, val])
4791                        }
4792                        _ => h.new_str(*name),
4793                    })
4794                    .collect();
4795                h.new_array(out)
4796            }));
4797        }
4798        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
4799        // keys are the members node-js implements, each resolved to the same
4800        // first-class value a property read would give.
4801        let mut names = crate::stdlib::namespace_keys(&ns);
4802        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
4803        // its members live in the builtin dispatch table. They are
4804        // non-enumerable in V8, so they surface only under
4805        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
4806        if names.is_empty() && mode == 3 {
4807            let prefix = format!("{ns}.");
4808            names = NS_METHODS
4809                .iter()
4810                .filter_map(|q| q.strip_prefix(&prefix))
4811                .map(|m| m.to_string())
4812                .collect();
4813        }
4814        if !names.is_empty() {
4815            let entries: Vec<(String, Value)> = names
4816                .into_iter()
4817                .map(|k| {
4818                    let val = namespace_property(&ns, &k);
4819                    (k, val)
4820                })
4821                .collect();
4822            return Ok(with_host(|h| {
4823                let out: Vec<Value> = entries
4824                    .into_iter()
4825                    .map(|(k, val)| match mode {
4826                        1 => val,
4827                        2 => {
4828                            let ks = h.new_str(k);
4829                            h.new_array(vec![ks, val])
4830                        }
4831                        _ => h.new_str(k),
4832                    })
4833                    .collect();
4834                h.new_array(out)
4835            }));
4836        }
4837    }
4838    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
4839    // non-enumerable ones, plus the exotic `length` an array carries.
4840    let entries: Vec<(String, Value)> = with_host(|h| {
4841        if mode == 3 {
4842            // An array's exotic `length` is already placed (after the indices,
4843            // before the ordinary string keys) by `own_key_names`.
4844            return h
4845                .own_key_names(&v, false)
4846                .into_iter()
4847                .map(|k| (k, Value::Undef))
4848                .collect();
4849        }
4850        Vec::new()
4851    });
4852    let entries = if mode == 3 {
4853        entries
4854    } else {
4855        host::own_enum_entries_deep(&v)
4856    };
4857    Ok(with_host(|h| {
4858        let out: Vec<Value> = entries
4859            .into_iter()
4860            .map(|(k, val)| match mode {
4861                0 | 3 => h.new_str(k),
4862                1 => val,
4863                _ => {
4864                    let ks = h.new_str(k);
4865                    h.new_array(vec![ks, val])
4866                }
4867            })
4868            .collect();
4869        h.new_array(out)
4870    }))
4871}
4872
4873fn object_assign(args: Vec<Value>) -> Result<Value, String> {
4874    let target = arg0(&args);
4875    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
4876    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
4877    require_object_coercible(&target)?;
4878    for src in args.iter().skip(1) {
4879        // `Object.assign` copies own *enumerable* properties, running any getter
4880        // — symbol-keyed ones included (7.3.25).
4881        let entries = host::own_enum_entries_deep(src);
4882        let syms = with_host(|h| h.own_symbol_entries(src));
4883        // A plain object target is filled in place (one borrow, then a single
4884        // re-canonicalization of the integer-index keys).
4885        let filled = with_host(|h| {
4886            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
4887                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
4888                    p.insert(k, v);
4889                }
4890                host::canonicalize_own_keys(p);
4891                return true;
4892            }
4893            false
4894        });
4895        // Any OTHER target — an array being the common one — goes through the
4896        // ordinary Set path. The in-place branch above matched `JsObj::Object`
4897        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
4898        // returned the untouched array: no error, just a missing property. The
4899        // Set path is what an `arr.extra = 9` assignment already used, so index
4900        // and non-index keys land where they do for a direct write.
4901        if !filled {
4902            for (k, v) in entries.into_iter().chain(syms) {
4903                set_property(&target, &k, v)?;
4904            }
4905        }
4906    }
4907    Ok(target)
4908}
4909
4910fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
4911    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
4912    let mut props: IndexMap<String, Value> = IndexMap::new();
4913    for p in pairs {
4914        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
4915        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
4916        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
4917        props.insert(key, val);
4918    }
4919    Ok(with_host(|h| h.new_object(props)))
4920}
4921
4922/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
4923/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
4924/// members mapped to that key, in first-seen key order.
4925fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
4926    let items = host::iter_all(&arg0(&args))?;
4927    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
4928    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
4929    for (i, item) in items.into_iter().enumerate() {
4930        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
4931        let key = with_host(|h| h.property_key(&key_v));
4932        groups.entry(key).or_default().push(item);
4933    }
4934    let props: IndexMap<String, Value> = with_host(|h| {
4935        groups
4936            .into_iter()
4937            .map(|(k, v)| (k, h.new_array(v)))
4938            .collect()
4939    });
4940    let obj = with_host(|h| h.new_object(props));
4941    // A null-prototype object (as Node returns), so it has no inherited members.
4942    with_host(|h| {
4943        let nv = h.null();
4944        h.set_proto(&obj, nv);
4945    });
4946    Ok(obj)
4947}
4948
4949/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
4950/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
4951fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
4952    let items = host::iter_all(&arg0(&args))?;
4953    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
4954    let m = with_host(|h| {
4955        h.alloc(JsObj::Map {
4956            entries: IndexMap::new(),
4957            weak: false,
4958        })
4959    });
4960    for (i, item) in items.into_iter().enumerate() {
4961        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
4962        let existing = map_method(&m, "get", vec![key_v.clone()])?;
4963        if matches!(existing, Value::Undef) {
4964            let arr = with_host(|h| h.new_array(vec![item]));
4965            map_method(&m, "set", vec![key_v, arr])?;
4966        } else {
4967            with_host(|h| {
4968                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
4969                    a.push(item);
4970                }
4971            });
4972        }
4973    }
4974    Ok(m)
4975}
4976
4977/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
4978/// element and each `mapFn` result.
4979///
4980/// Written in JavaScript and compiled once, because the operation IS an async
4981/// function: a Rust builtin runs outside any coroutine and has no way to await,
4982/// so draining a promise from there would mean running the microtask queue by
4983/// hand. Delegating to the engine's own `async`/`for await` keeps the
4984/// suspension semantics — and the ordering they imply — exactly the language's.
4985///
4986/// The source may be an async iterable, a sync iterable, a bare iterator, or an
4987/// array-like. Everything iterable goes through `for await`, which awaits a sync
4988/// source's elements individually — that is what makes
4989/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
4990/// accepted because an async generator object does not expose
4991/// `Symbol.asyncIterator` on this frontend.
4992fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
4993    thread_local! {
4994        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
4995    }
4996    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
4997        const out = []; let i = 0;\n\
4998        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
4999        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
5000            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
5001        if (iterable) {\n\
5002            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
5003            return out;\n\
5004        }\n\
5005        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
5006        while (i < len) { await step(items[i]); }\n\
5007        return out;\n\
5008    })";
5009    let f = IMPL.with(|c| c.borrow().clone());
5010    let f = match f {
5011        Some(f) => f,
5012        None => {
5013            let f = crate::eval_in_global_scope(SRC)?;
5014            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
5015            f
5016        }
5017    };
5018    host::invoke(&f, args, None)
5019}
5020
5021fn array_from(args: Vec<Value>) -> Result<Value, String> {
5022    // `Array.from` accepts generators and user iterables, plus array-likes with a
5023    // numeric `.length`.
5024    let src = arg0(&args);
5025    let items = match host::iter_all(&src) {
5026        Ok(v) => v,
5027        Err(_) => array_like_items(&src),
5028    };
5029    if let Some(cb) = args.get(1).cloned() {
5030        let mut out = Vec::with_capacity(items.len());
5031        for (i, it) in items.into_iter().enumerate() {
5032            out.push(host::invoke(&cb, vec![it, Value::Float(i as f64)], None)?);
5033        }
5034        return Ok(with_host(|h| h.new_array(out)));
5035    }
5036    Ok(with_host(|h| h.new_array(items)))
5037}
5038
5039/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
5040fn array_like_items(src: &Value) -> Vec<Value> {
5041    let len = get_property(src, "length")
5042        .ok()
5043        .map(|l| with_host(|h| h.to_number(&l)))
5044        .unwrap_or(0.0);
5045    if !len.is_finite() || len <= 0.0 {
5046        return Vec::new();
5047    }
5048    (0..len as usize)
5049        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
5050        .collect()
5051}
5052
5053// ── JSON ──────────────────────────────────────────────────────────────────────
5054
5055fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
5056    // A CALLABLE second argument is the replacer function, and it is checked
5057    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
5058    // callable never also reaches the key-filter path below.
5059    let replacer = args
5060        .get(1)
5061        .filter(|r| with_host(|h| host::is_callable(h, r)))
5062        .cloned();
5063    // `toJSON` and the replacer run BEFORE serialization and are user code, so
5064    // the tree is rewritten first — outside the host borrow `json_str` holds,
5065    // and before the BigInt walk, which has no cycle guard of its own.
5066    //
5067    // The top-level value is a property of a synthetic wrapper `{ "": value }`
5068    // under key `""`, which is exactly the holder the replacer receives as
5069    // `this` on its first call.
5070    let root = arg0(&args);
5071    let wrapper = with_host(|h| {
5072        let mut m: IndexMap<String, Value> = IndexMap::new();
5073        m.insert(String::new(), root.clone());
5074        h.new_object(m)
5075    });
5076    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
5077    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
5078    // bigint form), matching Node's exact message.
5079    if with_host(|h| json_has_bigint(h, &v)) {
5080        return Err(host::type_error("Do not know how to serialize a BigInt"));
5081    }
5082    let indent = match args.get(2) {
5083        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
5084        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
5085        None => String::new(),
5086    };
5087    // A replacer array (args[1]) restricts which object keys are serialized.
5088    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
5089        with_host(|h| match h.get(r) {
5090            Some(JsObj::Array(items)) => {
5091                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
5092            }
5093            _ => None,
5094        })
5095    });
5096    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
5097    match s {
5098        Some(s) => Ok(with_host(|h| h.new_str(s))),
5099        None => Ok(Value::Undef),
5100    }
5101}
5102
5103/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
5104/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
5105/// function as `replacer.call(holder, key, value)`, then recurse into whatever
5106/// object survives. Applies to user methods, class methods, and the native
5107/// `Date`/`Buffer`/`URL` accessors alike.
5108///
5109/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
5110/// objects currently being walked so a cyclic structure is reported rather than
5111/// spinning forever.
5112///
5113/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
5114/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
5115/// because the inner method is a plain (unserializable) function property of the
5116/// returned object, not a second conversion hook.
5117fn apply_to_json(
5118    holder: &Value,
5119    key: &str,
5120    v: &Value,
5121    path: &mut Vec<Value>,
5122    rep: Option<&Value>,
5123) -> Result<Value, String> {
5124    let mut v = v.clone();
5125    if matches!(v, Value::Obj(_)) {
5126        let tag = crate::stdlib::native_tag(&v);
5127        let has_to_json = with_host(|h| match host::lookup_chain(h, &v, "toJSON") {
5128            Some(f) => host::is_callable(h, &f),
5129            None => false,
5130        }) || tag
5131            .as_deref()
5132            .map(crate::stdlib::has_to_json)
5133            .unwrap_or(false);
5134        if has_to_json {
5135            let k = with_host(|h| h.new_str(key.to_string()));
5136            v = host::call_method(&v, "toJSON", vec![k])?;
5137        }
5138    }
5139    if let Some(rep) = rep {
5140        let k = with_host(|h| h.new_str(key.to_string()));
5141        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
5142    }
5143    json_walk_children(&v, path, rep)
5144}
5145
5146/// Whether a raw property key of a host object is one `json_str` serializes. The
5147/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
5148/// invisible to JSON, so the replacer must not be invoked for them either.
5149fn json_visible_key(k: &str) -> bool {
5150    !k.starts_with("@@") && !k.starts_with('#')
5151}
5152
5153/// Recurse into the elements/properties of an already-converted value, running
5154/// `apply_to_json` for each with this value as the holder.
5155fn json_walk_children(
5156    v: &Value,
5157    path: &mut Vec<Value>,
5158    rep: Option<&Value>,
5159) -> Result<Value, String> {
5160    if !matches!(v, Value::Obj(_)) {
5161        return Ok(v.clone());
5162    }
5163    // A value that contains itself has no JSON form.
5164    if with_host(|h| path.iter().any(|p| h.strict_eq(p, v))) {
5165        return Err(host::type_error("Converting circular structure to JSON"));
5166    }
5167    // A Proxy owns no property map, so it is snapshotted through its traps into
5168    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
5169    // — which read every member through `[[Get]]`, exactly as the snapshot does.
5170    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
5171        let snap = crate::proxy::json_snapshot(v)?;
5172        path.push(v.clone());
5173        let out = json_walk_children(&snap, path, rep);
5174        path.pop();
5175        return out;
5176    }
5177    let obj = with_host(|h| h.get(v).cloned());
5178    path.push(v.clone());
5179    let out = (|| match obj {
5180        Some(JsObj::Array(items)) => {
5181            let mut out = Vec::with_capacity(items.len());
5182            let mut changed = false;
5183            for (i, it) in items.iter().enumerate() {
5184                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
5185                changed |= !with_host(|h| h.strict_eq(&nv, it));
5186                out.push(nv);
5187            }
5188            // Keep identity when nothing changed, so an enclosing object is not
5189            // needlessly rebuilt (which would drop its property attributes).
5190            if changed {
5191                Ok(with_host(|h| h.new_array(out)))
5192            } else {
5193                Ok(v.clone())
5194            }
5195        }
5196        Some(JsObj::Object(props)) => {
5197            // An enumerable own accessor must have its getter RUN and the result
5198            // serialized. That cannot happen inside `json_str` (which holds the
5199            // host borrow), so materialize here — the same reason `toJSON` is
5200            // applied in this pass.
5201            let has_accessor = with_host(|h| {
5202                h.own_accessor_keys(v)
5203                    .iter()
5204                    .any(|k| h.prop_attrs(v, k).enumerable)
5205            });
5206            if has_accessor {
5207                let mut next: IndexMap<String, Value> = IndexMap::new();
5208                for (k, val) in host::own_enum_entries_deep(v) {
5209                    let nv = if json_visible_key(&k) {
5210                        apply_to_json(v, &k, &val, path, rep)?
5211                    } else {
5212                        val
5213                    };
5214                    next.insert(k, nv);
5215                }
5216                return Ok(with_host(|h| h.new_object(next)));
5217            }
5218            // Only rebuild when a descendant actually changed, so plain data keeps
5219            // its identity (and its prototype / native tag).
5220            let mut next: IndexMap<String, Value> = IndexMap::new();
5221            let mut changed = false;
5222            for (k, val) in &props {
5223                let nv = if json_visible_key(k) {
5224                    apply_to_json(v, k, val, path, rep)?
5225                } else {
5226                    val.clone()
5227                };
5228                changed |= !with_host(|h| h.strict_eq(&nv, val));
5229                next.insert(k.clone(), nv);
5230            }
5231            if changed {
5232                Ok(with_host(|h| {
5233                    let o = h.new_object(next);
5234                    h.copy_prop_attrs(v, &o);
5235                    o
5236                }))
5237            } else {
5238                Ok(v.clone())
5239            }
5240        }
5241        _ => Ok(v.clone()),
5242    })();
5243    path.pop();
5244    out
5245}
5246
5247/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
5248/// try to serialize (a value in an array/object) — such a value throws.
5249fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
5250    match h.get(v) {
5251        Some(JsObj::BigInt(_)) => true,
5252        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
5253        Some(JsObj::Object(props)) => props
5254            .iter()
5255            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
5256            .any(|(_, val)| json_has_bigint(h, val)),
5257        _ => false,
5258    }
5259}
5260
5261fn json_str(
5262    h: &host::JsHost,
5263    v: &Value,
5264    indent: &str,
5265    depth: usize,
5266    keys: Option<&[String]>,
5267) -> Option<String> {
5268    let sep = if indent.is_empty() { ":" } else { ": " };
5269    match v {
5270        Value::Undef => None,
5271        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
5272        Value::Int(n) => Some(n.to_string()),
5273        Value::Float(f) => Some(if f.is_finite() {
5274            host::fmt_number(*f)
5275        } else {
5276            "null".into()
5277        }),
5278        Value::Str(s) => Some(json_quote(s)),
5279        Value::Obj(_) => match h.get(v) {
5280            Some(JsObj::Str(s)) => Some(json_quote(s)),
5281            Some(JsObj::Null) => Some("null".into()),
5282            // Map/Set have no enumerable own string keys → serialize as `{}`.
5283            Some(JsObj::Map { .. }) | Some(JsObj::Set { .. }) => Some("{}".into()),
5284            // Functions and symbols are omitted (undefined) as values.
5285            Some(JsObj::Func(_))
5286            | Some(JsObj::Builtin(_))
5287            | Some(JsObj::BoundMethod { .. })
5288            | Some(JsObj::BoundFunc { .. })
5289            | Some(JsObj::Class(_))
5290            | Some(JsObj::Symbol { .. })
5291            | Some(JsObj::Generator { .. }) => None,
5292            Some(JsObj::Array(items)) => {
5293                if items.is_empty() {
5294                    return Some("[]".into());
5295                }
5296                let parts: Vec<String> = items
5297                    .iter()
5298                    .map(|x| {
5299                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
5300                    })
5301                    .collect();
5302                Some(wrap(&parts, "[", "]", indent, depth))
5303            }
5304            Some(JsObj::Object(props)) => {
5305                // A replacer array restricts (and orders) which keys are emitted.
5306                let parts: Vec<String> = match keys {
5307                    Some(allow) => allow
5308                        .iter()
5309                        .filter_map(|k| {
5310                            props.get(k).and_then(|val| {
5311                                json_str(h, val, indent, depth + 1, keys)
5312                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
5313                            })
5314                        })
5315                        .collect(),
5316                    None => h
5317                        .own_enum_entries(v)
5318                        .iter()
5319                        .filter_map(|(k, val)| {
5320                            json_str(h, val, indent, depth + 1, keys)
5321                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
5322                        })
5323                        .collect(),
5324                };
5325                if parts.is_empty() {
5326                    return Some("{}".into());
5327                }
5328                Some(wrap(&parts, "{", "}", indent, depth))
5329            }
5330            _ => Some("null".into()),
5331        },
5332        _ => Some("null".into()),
5333    }
5334}
5335
5336fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
5337    if indent.is_empty() {
5338        format!("{open}{}{close}", parts.join(","))
5339    } else {
5340        let pad = indent.repeat(depth + 1);
5341        let pad_close = indent.repeat(depth);
5342        format!(
5343            "{open}\n{pad}{}\n{pad_close}{close}",
5344            parts.join(&format!(",\n{pad}"))
5345        )
5346    }
5347}
5348
5349fn json_quote(s: &str) -> String {
5350    let mut out = String::from("\"");
5351    for c in s.chars() {
5352        match c {
5353            '"' => out.push_str("\\\""),
5354            '\\' => out.push_str("\\\\"),
5355            '\n' => out.push_str("\\n"),
5356            '\t' => out.push_str("\\t"),
5357            '\r' => out.push_str("\\r"),
5358            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
5359            // Backspace and form feed were missing, so they fell through to the
5360            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
5361            // `""` where node produces `"\b"`. Both parse back to the same
5362            // string, so the difference is invisible to a round trip and shows
5363            // up only as a byte mismatch against a fixture or a checksum.
5364            '\u{8}' => out.push_str("\\b"),
5365            '\u{c}' => out.push_str("\\f"),
5366            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
5367            _ => out.push(c),
5368        }
5369    }
5370    out.push('"');
5371    out
5372}
5373
5374fn json_parse(args: Vec<Value>) -> Result<Value, String> {
5375    let s = with_host(|h| h.str_of(&arg0(&args)));
5376    let mut p = JsonParser {
5377        chars: s.chars().collect(),
5378        pos: 0,
5379    };
5380    p.skip_ws();
5381    if p.peek().is_none() {
5382        return Err("SyntaxError: Unexpected end of JSON input".into());
5383    }
5384    let v = p.parse_value()?;
5385    let value_end = p.pos;
5386    p.skip_ws();
5387    // Anything after the top-level value is an error — the parser used to accept
5388    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
5389    if let Some(c) = p.peek() {
5390        // V8 names the token kind only when it butts directly against the value
5391        // (`01` -> "Unexpected number at position 1"); with whitespace between
5392        // it is just a non-whitespace character (`1 2`).
5393        // Only a digit butted directly against a completed number literal —
5394        // V8's number scanner is still in number context there. `5"x"` and
5395        // `[0,1]0` exit the scanner cleanly and get the generic message.
5396        let after_number = value_end > 0
5397            && p.pos == value_end
5398            && p.chars[value_end - 1].is_ascii_digit()
5399            && c.is_ascii_digit();
5400        return Err(if after_number {
5401            p.err_at("Unexpected number", p.pos)
5402        } else {
5403            p.err_trailing(p.pos)
5404        });
5405    }
5406    // Optional reviver: walk bottom-up, transforming each (key, value).
5407    if let Some(reviver) = args
5408        .get(1)
5409        .filter(|r| with_host(|h| host::is_callable(h, r)))
5410        .cloned()
5411    {
5412        return json_revive("", v, &reviver);
5413    }
5414    Ok(v)
5415}
5416
5417/// `JSON.parse` reviver walk: recurse into children first, then call
5418/// `reviver(key, value)`; a returned `undefined` drops the property.
5419fn json_revive(key: &str, val: Value, reviver: &Value) -> Result<Value, String> {
5420    match with_host(|h| h.get(&val).cloned()) {
5421        Some(JsObj::Array(items)) => {
5422            for i in 0..items.len() {
5423                let elem = with_host(|h| match h.get(&val) {
5424                    Some(JsObj::Array(it)) => it[i].clone(),
5425                    _ => Value::Undef,
5426                });
5427                let nv = json_revive(&i.to_string(), elem, reviver)?;
5428                with_host(|h| {
5429                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
5430                        it[i] = nv;
5431                    }
5432                });
5433            }
5434        }
5435        Some(JsObj::Object(props)) => {
5436            let keys: Vec<String> = props
5437                .keys()
5438                .filter(|k| !k.starts_with("@@"))
5439                .cloned()
5440                .collect();
5441            for k in keys {
5442                let elem = with_host(|h| match h.get(&val) {
5443                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
5444                    _ => Value::Undef,
5445                });
5446                let nv = json_revive(&k, elem, reviver)?;
5447                with_host(|h| {
5448                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
5449                        if matches!(nv, Value::Undef) {
5450                            p.shift_remove(&k);
5451                        } else {
5452                            p.insert(k.clone(), nv);
5453                        }
5454                    }
5455                });
5456            }
5457        }
5458        _ => {}
5459    }
5460    let kv = with_host(|h| h.new_str(key.to_string()));
5461    host::invoke(reviver, vec![kv, val], None)
5462}
5463
5464struct JsonParser {
5465    chars: Vec<char>,
5466    pos: usize,
5467}
5468impl JsonParser {
5469    fn peek(&self) -> Option<char> {
5470        self.chars.get(self.pos).copied()
5471    }
5472
5473    /// `at position N (line L column C)` — the location suffix V8 appends to the
5474    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
5475    /// node-js counts `char`s, which agree for the BMP.
5476    fn at(&self, pos: usize) -> String {
5477        let mut line = 1usize;
5478        let mut col = 1usize;
5479        for c in &self.chars[..pos.min(self.chars.len())] {
5480            if *c == '\n' {
5481                line += 1;
5482                col = 1;
5483            } else {
5484                col += 1;
5485            }
5486        }
5487        format!(" at position {pos} (line {line} column {col})")
5488    }
5489
5490    /// A positional error (`Expected ':' after property name in JSON at …`).
5491    fn err_at(&self, what: &str, pos: usize) -> String {
5492        format!("SyntaxError: {what} in JSON{}", self.at(pos))
5493    }
5494
5495    /// The one positional message V8 does NOT suffix with `in JSON`.
5496    fn err_trailing(&self, pos: usize) -> String {
5497        format!(
5498            "SyntaxError: Unexpected non-whitespace character after JSON{}",
5499            self.at(pos)
5500        )
5501    }
5502
5503    /// V8's default parse error: the offending character plus a window of the
5504    /// source. The whole input is quoted when it is short (<= 20 chars);
5505    /// otherwise a 10-character context window either side of `pos` is shown,
5506    /// elided with `...` on whichever side was cut.
5507    fn err_token(&self, pos: usize) -> String {
5508        const MAX_WHOLE: usize = 20;
5509        const CONTEXT: usize = 10;
5510        let len = self.chars.len();
5511        let Some(c) = self.chars.get(pos) else {
5512            return "SyntaxError: Unexpected end of JSON input".into();
5513        };
5514        // V8 reports the whole input for the JS literals that are famously not
5515        // JSON, without naming an offending character.
5516        let whole: String = self.chars.iter().collect();
5517        if matches!(
5518            whole.as_str(),
5519            "undefined" | "NaN" | "Infinity" | "-Infinity"
5520        ) {
5521            return format!("SyntaxError: \"{whole}\" is not valid JSON");
5522        }
5523        let snippet = if len <= MAX_WHOLE {
5524            format!("\"{whole}\"")
5525        } else {
5526            let start = pos.saturating_sub(CONTEXT);
5527            let end = (pos + CONTEXT).min(len);
5528            let body: String = self.chars[start..end].iter().collect();
5529            let head = if start > 0 { "..." } else { "" };
5530            let tail = if end < len { "..." } else { "" };
5531            format!("{head}\"{body}\"{tail}")
5532        };
5533        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
5534    }
5535
5536    fn skip_ws(&mut self) {
5537        while matches!(
5538            self.peek(),
5539            Some(' ') | Some('\n') | Some('\t') | Some('\r')
5540        ) {
5541            self.pos += 1;
5542        }
5543    }
5544    fn parse_value(&mut self) -> Result<Value, String> {
5545        self.skip_ws();
5546        match self.peek() {
5547            Some('{') => self.parse_object(),
5548            Some('[') => self.parse_array(),
5549            Some('"') => {
5550                let s = self.parse_string()?;
5551                Ok(with_host(|h| h.new_str(s)))
5552            }
5553            Some('t') | Some('f') => self.parse_bool(),
5554            Some('n') => {
5555                self.expect_lit("null")?;
5556                Ok(with_host(|h| h.null()))
5557            }
5558            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
5559            None => Err("SyntaxError: Unexpected end of JSON input".into()),
5560            _ => Err(self.err_token(self.pos)),
5561        }
5562    }
5563    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
5564        for ch in lit.chars() {
5565            match self.peek() {
5566                Some(c) if c == ch => self.pos += 1,
5567                // V8 reports the first character that broke the literal, which is
5568                // why `foo` complains about `'o'` (index 2) and not `'f'`.
5569                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
5570                _ => return Err(self.err_token(self.pos)),
5571            }
5572        }
5573        Ok(())
5574    }
5575    fn parse_bool(&mut self) -> Result<Value, String> {
5576        if self.peek() == Some('t') {
5577            self.expect_lit("true")?;
5578            Ok(Value::Bool(true))
5579        } else {
5580            self.expect_lit("false")?;
5581            Ok(Value::Bool(false))
5582        }
5583    }
5584    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
5585    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
5586    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
5587    /// it. Each way the grammar can run out has its own message.
5588    fn parse_number(&mut self) -> Result<Value, String> {
5589        let start = self.pos;
5590        if self.peek() == Some('-') {
5591            self.pos += 1;
5592            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5593                return Err(self.err_at("No number after minus sign", self.pos));
5594            }
5595        }
5596        if self.peek() == Some('0') {
5597            self.pos += 1;
5598        } else {
5599            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5600                self.pos += 1;
5601            }
5602        }
5603        if self.peek() == Some('.') {
5604            self.pos += 1;
5605            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5606                return Err(self.err_at("Unterminated fractional number", self.pos));
5607            }
5608            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5609                self.pos += 1;
5610            }
5611        }
5612        if matches!(self.peek(), Some('e') | Some('E')) {
5613            self.pos += 1;
5614            if matches!(self.peek(), Some('+') | Some('-')) {
5615                self.pos += 1;
5616            }
5617            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5618                return Err(self.err_at("Exponent part is missing a number", self.pos));
5619            }
5620            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
5621                self.pos += 1;
5622            }
5623        }
5624        let s: String = self.chars[start..self.pos].iter().collect();
5625        s.parse::<f64>()
5626            .map(Value::Float)
5627            .map_err(|_| self.err_at("Unexpected number", start))
5628    }
5629    fn parse_string(&mut self) -> Result<String, String> {
5630        self.pos += 1; // opening quote
5631        let mut out = String::new();
5632        loop {
5633            match self.peek() {
5634                None => return Err(self.err_at("Unterminated string", self.pos)),
5635                Some('"') => {
5636                    self.pos += 1;
5637                    break;
5638                }
5639                Some('\\') => {
5640                    self.pos += 1;
5641                    match self.peek() {
5642                        Some('n') => out.push('\n'),
5643                        Some('t') => out.push('\t'),
5644                        Some('r') => out.push('\r'),
5645                        Some('"') => out.push('"'),
5646                        Some('\\') => out.push('\\'),
5647                        Some('/') => out.push('/'),
5648                        Some('b') => out.push('\u{08}'),
5649                        Some('f') => out.push('\u{0C}'),
5650                        Some('u') => {
5651                            let h: String = self.chars
5652                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
5653                                .iter()
5654                                .collect();
5655                            if let Ok(n) = u32::from_str_radix(&h, 16) {
5656                                if let Some(ch) = char::from_u32(n) {
5657                                    out.push(ch);
5658                                }
5659                            }
5660                            self.pos += 4;
5661                        }
5662                        _ => {}
5663                    }
5664                    self.pos += 1;
5665                }
5666                // A raw control character is not legal inside a JSON string; it
5667                // has to be escaped. V8 rejects it rather than passing it through.
5668                Some(c) if (c as u32) < 0x20 => {
5669                    return Err(self.err_at("Bad control character in string literal", self.pos))
5670                }
5671                Some(c) => {
5672                    out.push(c);
5673                    self.pos += 1;
5674                }
5675            }
5676        }
5677        Ok(out)
5678    }
5679    fn parse_array(&mut self) -> Result<Value, String> {
5680        self.pos += 1; // [
5681        let mut items = Vec::new();
5682        self.skip_ws();
5683        if self.peek() == Some(']') {
5684            self.pos += 1;
5685            return Ok(with_host(|h| h.new_array(items)));
5686        }
5687        loop {
5688            items.push(self.parse_value()?);
5689            self.skip_ws();
5690            match self.peek() {
5691                Some(',') => {
5692                    self.pos += 1;
5693                }
5694                Some(']') => {
5695                    self.pos += 1;
5696                    break;
5697                }
5698                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
5699            }
5700        }
5701        Ok(with_host(|h| h.new_array(items)))
5702    }
5703    fn parse_object(&mut self) -> Result<Value, String> {
5704        self.pos += 1; // {
5705        let mut props: IndexMap<String, Value> = IndexMap::new();
5706        self.skip_ws();
5707        if self.peek() == Some('}') {
5708            self.pos += 1;
5709            return Ok(with_host(|h| h.new_object(props)));
5710        }
5711        loop {
5712            self.skip_ws();
5713            if self.peek() != Some('"') {
5714                // The first key uses the "or '}'" wording (an empty object is
5715                // still legal there); a key after a comma does not. End of input
5716                // reports the same expectation, at the end position.
5717                return Err(if props.is_empty() {
5718                    self.err_at("Expected property name or '}'", self.pos)
5719                } else {
5720                    self.err_at("Expected double-quoted property name", self.pos)
5721                });
5722            }
5723            let key = self.parse_string()?;
5724            self.skip_ws();
5725            if self.peek() != Some(':') {
5726                return Err(match self.peek() {
5727                    None => "SyntaxError: Unexpected end of JSON input".into(),
5728                    _ => self.err_at("Expected ':' after property name", self.pos),
5729                });
5730            }
5731            self.pos += 1;
5732            let val = self.parse_value()?;
5733            props.insert(key, val);
5734            self.skip_ws();
5735            match self.peek() {
5736                Some(',') => {
5737                    self.pos += 1;
5738                }
5739                Some('}') => {
5740                    self.pos += 1;
5741                    break;
5742                }
5743                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
5744            }
5745        }
5746        Ok(with_host(|h| h.new_object(props)))
5747    }
5748}
5749
5750// ══ type methods (array / string / number) ═══════════════════════════════════
5751
5752fn is_array_method(name: &str) -> bool {
5753    matches!(
5754        name,
5755        "push"
5756            | "pop"
5757            | "shift"
5758            | "unshift"
5759            | "map"
5760            | "filter"
5761            | "forEach"
5762            | "join"
5763            | "slice"
5764            | "indexOf"
5765            | "lastIndexOf"
5766            | "includes"
5767            | "reduce"
5768            | "concat"
5769            | "reverse"
5770            | "sort"
5771            | "find"
5772            | "findIndex"
5773            | "some"
5774            | "every"
5775            | "flat"
5776            | "fill"
5777            | "splice"
5778            | "keys"
5779            | "values"
5780            | "entries"
5781            | "flatMap"
5782            | "at"
5783            | "toString"
5784            | "reduceRight"
5785            | "findLast"
5786            | "findLastIndex"
5787            | "copyWithin"
5788    )
5789}
5790fn is_string_method(name: &str) -> bool {
5791    matches!(
5792        name,
5793        "toUpperCase"
5794            | "toLowerCase"
5795            | "charAt"
5796            | "charCodeAt"
5797            | "codePointAt"
5798            | "indexOf"
5799            | "lastIndexOf"
5800            | "includes"
5801            | "slice"
5802            | "substring"
5803            | "substr"
5804            | "split"
5805            | "trim"
5806            | "trimStart"
5807            | "trimEnd"
5808            | "replace"
5809            | "replaceAll"
5810            | "repeat"
5811            | "startsWith"
5812            | "endsWith"
5813            | "padStart"
5814            | "padEnd"
5815            | "concat"
5816            | "at"
5817            | "toString"
5818            | "toLocaleString"
5819            | "valueOf"
5820            | "match"
5821            | "matchAll"
5822            | "search"
5823            | "normalize"
5824            | "localeCompare"
5825            | "toLocaleUpperCase"
5826            | "toLocaleLowerCase"
5827            | "isWellFormed"
5828            | "toWellFormed"
5829    )
5830}
5831
5832/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
5833fn is_regexp_arg(v: &Value) -> bool {
5834    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
5835}
5836
5837/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
5838/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
5839fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
5840    if pat.is_empty() {
5841        return Ok(s.to_string());
5842    }
5843    let mut out = String::new();
5844    let mut rest = s;
5845    let mut base = 0usize;
5846    while let Some(pos) = rest.find(pat) {
5847        out.push_str(&rest[..pos]);
5848        let offset = base + pos;
5849        let m = with_host(|h| h.new_str(pat.to_string()));
5850        let str_arg = with_host(|h| h.new_str(s.to_string()));
5851        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
5852        out.push_str(&with_host(|h| h.str_of(&r)));
5853        let consumed = pos + pat.len();
5854        base += consumed;
5855        rest = &rest[consumed..];
5856        if !all {
5857            break;
5858        }
5859    }
5860    out.push_str(rest);
5861    Ok(out)
5862}
5863fn is_number_method(name: &str) -> bool {
5864    matches!(
5865        name,
5866        "toFixed" | "toExponential" | "toString" | "toPrecision" | "toLocaleString" | "valueOf"
5867    )
5868}
5869
5870/// Dispatch `recv.name(args)` for the built-in prototype methods.
5871pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
5872    // `Object.prototype.valueOf` is inherited by every exotic that does not
5873    // override it (an Array does not), and returns the receiver. Without this
5874    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
5875    // and threw `valueOf is not a function`.
5876    if name == "valueOf"
5877        && matches!(
5878            with_host(|h| h.kind_of(recv)),
5879            Some(
5880                ObjKind::Array
5881                    | ObjKind::Map
5882                    | ObjKind::Set
5883                    | ObjKind::Generator
5884                    | ObjKind::Promise
5885                    | ObjKind::Iter
5886                    | ObjKind::RegExp
5887            )
5888        )
5889    {
5890        return Ok(recv.clone());
5891    }
5892    // Only the tag is needed to pick the branch — cloning the receiver here made
5893    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
5894    match with_host(|h| h.kind_of(recv)) {
5895        Some(ObjKind::Array) => array_method(recv, name, args),
5896        Some(ObjKind::Str) => {
5897            // `string_method` consumes the text itself, so this clone is the
5898            // payload, not a tag probe.
5899            let s = peek(recv, |o| match o {
5900                JsObj::Str(s) => Some(s.clone()),
5901                _ => None,
5902            })
5903            .unwrap_or_default();
5904            string_method(&s, name, args)
5905        }
5906        Some(ObjKind::Map) => map_method(recv, name, args),
5907        Some(ObjKind::Set) => set_method(recv, name, args),
5908        Some(ObjKind::Generator) => generator_method(recv, name, args),
5909        Some(ObjKind::Promise) => promise_method(recv, name, args),
5910        Some(ObjKind::Iter) => iter_method(recv, name, args),
5911        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
5912        Some(ObjKind::BigInt) => {
5913            let b = peek(recv, |o| match o {
5914                JsObj::BigInt(b) => Some(b.clone()),
5915                _ => None,
5916            })
5917            .unwrap_or_default();
5918            bigint_method(&b, name, args)
5919        }
5920        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
5921        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
5922            match function_builtin_method(recv, name, &args)? {
5923                Some(v) => Ok(v),
5924                None => Err(host::type_error(&format!("{name} is not a function"))),
5925            }
5926        }
5927        Some(ObjKind::Object) => {
5928            if let Some(f) = peek(recv, |o| match o {
5929                JsObj::Object(p) => p.get(name).cloned(),
5930                _ => None,
5931            }) {
5932                host::invoke(&f, args, Some(recv.clone()))
5933            } else if name == "hasOwnProperty" {
5934                let k = with_host(|h| h.str_of(&arg0(&args)));
5935                let has = peek(recv, |o| match o {
5936                    JsObj::Object(p) => Some(p.contains_key(&k)),
5937                    _ => None,
5938                })
5939                .unwrap_or(false);
5940                Ok(Value::Bool(has))
5941            } else if name == "toString" {
5942                Ok(with_host(|h| h.new_str("[object Object]")))
5943            } else {
5944                Err(host::type_error(&format!("{} is not a function", name)))
5945            }
5946        }
5947        _ => {
5948            // Primitive number/bool/string coercions.
5949            if let Value::Float(_) | Value::Int(_) = recv {
5950                return number_method(with_host(|h| h.to_number(recv)), name, args);
5951            }
5952            if let Some(s) = with_host(|h| h.as_str(recv)) {
5953                return string_method(&s, name, args);
5954            }
5955            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
5956            // so it reached no branch at all and `true.toString()` threw `is not
5957            // a function`. Its three methods are `toString`, `valueOf`, and the
5958            // inherited `Object.prototype.toLocaleString` — which
5959            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
5960            // was reachable from the array form too.
5961            if let Value::Bool(b) = recv {
5962                return match name {
5963                    "toString" | "toLocaleString" => {
5964                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
5965                    }
5966                    "valueOf" => Ok(Value::Bool(*b)),
5967                    _ => Err(host::type_error(&format!("{name} is not a function"))),
5968                };
5969            }
5970            Err(host::type_error(&format!("{} is not a function", name)))
5971        }
5972    }
5973}
5974
5975/// A copy of the whole backing store, for the methods that genuinely consume
5976/// every element (`map`, `filter`, `join`, …). Never call it just to read
5977/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
5978fn array_items(recv: &Value) -> Vec<Value> {
5979    with_host(|h| match h.get(recv) {
5980        Some(JsObj::Array(items)) => items.clone(),
5981        _ => Vec::new(),
5982    })
5983}
5984
5985/// The ELIDED positions of array `recv` as a membership set. A dense array —
5986/// which is nearly every array — answers with an empty set after a single
5987/// negative hash probe and allocates nothing.
5988///
5989/// The iteration methods split into two groups, and the split is not a matter of
5990/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
5991/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
5992/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
5993/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
5994fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
5995    with_host(|h| h.hole_indices(recv)).into_iter().collect()
5996}
5997
5998/// The element count, without copying the elements.
5999fn array_len(recv: &Value) -> usize {
6000    peek(recv, |o| match o {
6001        JsObj::Array(items) => Some(items.len()),
6002        _ => None,
6003    })
6004    .unwrap_or(0)
6005}
6006
6007fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6008    array_method_on(recv, recv, name, args)
6009}
6010
6011/// The `Array.prototype` methods that WRITE to their receiver, and so need the
6012/// generic path to copy the result back onto the array-like.
6013const ARRAY_MUTATORS: &[&str] = &[
6014    "push",
6015    "pop",
6016    "shift",
6017    "unshift",
6018    "splice",
6019    "sort",
6020    "reverse",
6021    "fill",
6022    "copyWithin",
6023];
6024
6025/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
6026/// a DOM-ish collection, `arguments`).
6027///
6028/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
6029/// rather than over an Array's element vector, so the receiver only has to have
6030/// a `length`. The elements are read out into a temporary Array, the ordinary
6031/// implementation runs on that, and a MUTATING method writes the result back —
6032/// which keeps one implementation of each method rather than a second, generic
6033/// one that could drift from it.
6034///
6035/// An index the receiver does not own is a HOLE in the temporary, so the
6036/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
6037fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
6038    let len = match get_property(recv, "length") {
6039        Ok(v) => host::to_array_length(&v).unwrap_or(0),
6040        Err(_) => 0,
6041    };
6042    // A STRING receiver owns every index of its length; `has_property` answers
6043    // for objects and reports none of them, which made `[].map.call('abc', f)`
6044    // an array of three holes.
6045    let dense = with_host(|h| h.as_str(recv)).is_some();
6046    let mut items = Vec::with_capacity(len);
6047    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
6048    for i in 0..len {
6049        let k = i.to_string();
6050        if dense || has_property(recv, &k)? {
6051            items.push(get_property(recv, &k)?);
6052        } else {
6053            holes.insert(i);
6054            items.push(Value::Undef);
6055        }
6056    }
6057    let tmp = with_host(|h| {
6058        let a = h.new_array(items);
6059        h.install_holes(&a, holes);
6060        a
6061    });
6062    let out = array_method_on(&tmp, recv, method, args)?;
6063    if ARRAY_MUTATORS.contains(&method) {
6064        let result = with_host(|h| match h.get(&tmp) {
6065            Some(JsObj::Array(items)) => items.clone(),
6066            _ => Vec::new(),
6067        });
6068        for (i, v) in result.iter().enumerate() {
6069            set_property(recv, &i.to_string(), v.clone())?;
6070        }
6071        set_property(recv, "length", Value::Float(result.len() as f64))?;
6072    }
6073    Ok(out)
6074}
6075
6076/// `Array.prototype.<name>` on `recv`.
6077///
6078/// `this_value` is what a callback receives as its third argument and what a
6079/// mutating method returns — the same object as `recv` for an ordinary array
6080/// call, but the ORIGINAL array-like when `array_generic` runs a method against
6081/// a temporary copy (`Array.prototype.slice.call(arguments)`).
6082fn array_method_on(
6083    recv: &Value,
6084    this_value: &Value,
6085    name: &str,
6086    args: Vec<Value>,
6087) -> Result<Value, String> {
6088    match name {
6089        "push" => {
6090            // `push` returns the new length; take it from the same mutable
6091            // borrow rather than copying the array back out to count it.
6092            let len = with_host(|h| {
6093                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6094                    items.extend(args.iter().cloned());
6095                    items.len()
6096                } else {
6097                    0
6098                }
6099            });
6100            Ok(Value::Float(len as f64))
6101        }
6102        "pop" => Ok(with_host(|h| {
6103            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6104                items.pop().unwrap_or(Value::Undef)
6105            } else {
6106                Value::Undef
6107            };
6108            let len = match h.get(recv) {
6109                Some(JsObj::Array(items)) => items.len(),
6110                _ => 0,
6111            };
6112            h.truncate_holes(recv, len);
6113            popped
6114        })),
6115        "shift" => Ok(with_host(|h| {
6116            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6117                if items.is_empty() {
6118                    Value::Undef
6119                } else {
6120                    items.remove(0)
6121                }
6122            } else {
6123                Value::Undef
6124            };
6125            h.remap_holes(recv, |i| i.checked_sub(1));
6126            shifted
6127        })),
6128        "unshift" => {
6129            with_host(|h| {
6130                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6131                    for (i, a) in args.iter().enumerate() {
6132                        items.insert(i, a.clone());
6133                    }
6134                }
6135                let n = args.len();
6136                h.remap_holes(recv, |i| Some(i + n));
6137            });
6138            Ok(Value::Float(array_len(recv) as f64))
6139        }
6140        "join" => {
6141            let sep = if args.is_empty() {
6142                ",".to_string()
6143            } else {
6144                with_host(|h| h.str_of(&args[0]))
6145            };
6146            join_array(recv, &sep)
6147        }
6148        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
6149        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
6150        // empty string. It threw `is not a function` — the whole method was
6151        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
6152        "toLocaleString" => {
6153            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
6154            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
6155            if !host::join_stack_push(recv) {
6156                return Ok(with_host(|h| h.new_str(String::new())));
6157            }
6158            let items = array_items(recv);
6159            let mut parts: Vec<String> = Vec::with_capacity(items.len());
6160            for it in &items {
6161                if with_host(|h| h.is_nullish(it)) {
6162                    parts.push(String::new());
6163                    continue;
6164                }
6165                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
6166                    Ok(v) => v,
6167                    Err(e) => {
6168                        host::join_stack_pop();
6169                        return Err(e);
6170                    }
6171                };
6172                parts.push(with_host(|h| h.str_of(&v)));
6173            }
6174            host::join_stack_pop();
6175            Ok(with_host(|h| h.new_str(parts.join(","))))
6176        }
6177        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
6178        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
6179        // `Get`-based `includes` reports `true` for the same array.
6180        "indexOf" => {
6181            let items = array_items(recv);
6182            let holes = hole_set(recv);
6183            let target = arg0(&args);
6184            let idx = with_host(|h| {
6185                items
6186                    .iter()
6187                    .enumerate()
6188                    .position(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
6189            });
6190            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
6191        }
6192        "lastIndexOf" => {
6193            let items = array_items(recv);
6194            let holes = hole_set(recv);
6195            let target = arg0(&args);
6196            let idx = with_host(|h| {
6197                items
6198                    .iter()
6199                    .enumerate()
6200                    .rposition(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
6201            });
6202            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
6203        }
6204        "includes" => {
6205            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
6206            let items = array_items(recv);
6207            let target = arg0(&args);
6208            let tnan = matches!(target, Value::Float(f) if f.is_nan());
6209            Ok(Value::Bool(with_host(|h| {
6210                items.iter().any(|x| {
6211                    (tnan && matches!(x, Value::Float(f) if f.is_nan())) || h.strict_eq(x, &target)
6212                })
6213            })))
6214        }
6215        "slice" => {
6216            let items = array_items(recv);
6217            let (lo, hi) = slice_bounds(&args, items.len());
6218            Ok(with_host(|h| {
6219                let out = h.new_array(items[lo..hi].to_vec());
6220                h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo));
6221                out
6222            }))
6223        }
6224        "concat" => {
6225            let mut out = array_items(recv);
6226            // A hole in either the receiver or a spreadable argument stays a hole
6227            // in the result, at its shifted position.
6228            let mut holes = hole_set(recv);
6229            let mut sources: Vec<(Value, usize)> = Vec::new();
6230            for a in &args {
6231                match with_host(|h| h.get(a).cloned()) {
6232                    Some(JsObj::Array(items)) => {
6233                        sources.push((a.clone(), out.len()));
6234                        out.extend(items);
6235                    }
6236                    _ => out.push(a.clone()),
6237                }
6238            }
6239            for (src, base) in sources {
6240                holes.extend(
6241                    with_host(|h| h.hole_indices(&src))
6242                        .into_iter()
6243                        .map(|i| i + base),
6244                );
6245            }
6246            Ok(with_host(|h| {
6247                let arr = h.new_array(out);
6248                h.install_holes(&arr, holes);
6249                arr
6250            }))
6251        }
6252        "reverse" => {
6253            let len = array_len(recv);
6254            with_host(|h| {
6255                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6256                    items.reverse();
6257                }
6258                h.remap_holes(recv, |i| Some(len - 1 - i));
6259            });
6260            Ok(this_value.clone())
6261        }
6262        "fill" => {
6263            // fill(value[, start[, end]]) — negative indices count from the end.
6264            let val = arg0(&args);
6265            let len = array_len(recv) as i64;
6266            let norm =
6267                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
6268            let start = if args.len() >= 2 {
6269                norm(arg_num(&args, 1) as i64)
6270            } else {
6271                0
6272            };
6273            let end = if args.len() >= 3 {
6274                norm(arg_num(&args, 2) as i64)
6275            } else {
6276                len as usize
6277            };
6278            with_host(|h| {
6279                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6280                    for it in items.iter_mut().take(end).skip(start) {
6281                        *it = val.clone();
6282                    }
6283                }
6284                // Every filled position now holds a real value.
6285                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
6286            });
6287            Ok(this_value.clone())
6288        }
6289        "copyWithin" => {
6290            // copyWithin(target, start[, end]) — copy a slice within the array.
6291            let items = array_items(recv);
6292            let len = items.len() as i64;
6293            let norm =
6294                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
6295            let target = norm(arg_num(&args, 0) as i64);
6296            let start = if args.len() >= 2 {
6297                norm(arg_num(&args, 1) as i64)
6298            } else {
6299                0
6300            };
6301            let end = if args.len() >= 3 {
6302                norm(arg_num(&args, 2) as i64)
6303            } else {
6304                len as usize
6305            };
6306            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
6307            let copied = slice.len();
6308            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
6309            // deletes the target when the source has no such property);
6310            // everything outside the written range keeps its own.
6311            let src_holes = hole_set(recv);
6312            with_host(|h| {
6313                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
6314                    for (k, v) in slice.into_iter().enumerate() {
6315                        if target + k < a.len() {
6316                            a[target + k] = v;
6317                        }
6318                    }
6319                }
6320                let len = len as usize;
6321                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
6322                    .iter()
6323                    .copied()
6324                    .filter(|i| *i < target || *i >= (target + copied).min(len))
6325                    .collect();
6326                for k in 0..copied {
6327                    if target + k < len && src_holes.contains(&(start + k)) {
6328                        holes.insert(target + k);
6329                    }
6330                }
6331                h.install_holes(recv, holes);
6332            });
6333            Ok(this_value.clone())
6334        }
6335        "at" => {
6336            let items = array_items(recv);
6337            let mut i = arg_num(&args, 0) as i64;
6338            if i < 0 {
6339                i += items.len() as i64;
6340            }
6341            Ok(if i >= 0 && (i as usize) < items.len() {
6342                items[i as usize].clone()
6343            } else {
6344                Value::Undef
6345            })
6346        }
6347        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
6348        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
6349        // twice and yields `[2, <1 empty item>, 6]`.
6350        "map" => {
6351            let items = array_items(recv);
6352            let holes = hole_set(recv);
6353            let cb = arg0(&args);
6354            let mut out = Vec::with_capacity(items.len());
6355            for (i, it) in items.iter().enumerate() {
6356                if holes.contains(&i) {
6357                    out.push(Value::Undef);
6358                    continue;
6359                }
6360                out.push(host::invoke(
6361                    &cb,
6362                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6363                    None,
6364                )?);
6365            }
6366            Ok(with_host(|h| {
6367                let arr = h.new_array(out);
6368                h.install_holes(&arr, holes);
6369                arr
6370            }))
6371        }
6372        "flatMap" => {
6373            let items = array_items(recv);
6374            let cb = arg0(&args);
6375            let holes = hole_set(recv);
6376            let mut out = Vec::new();
6377            for (i, it) in items.iter().enumerate() {
6378                if holes.contains(&i) {
6379                    continue;
6380                }
6381                let r = host::invoke(
6382                    &cb,
6383                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6384                    None,
6385                )?;
6386                match with_host(|h| h.get(&r).cloned()) {
6387                    Some(JsObj::Array(inner)) => out.extend(inner),
6388                    _ => out.push(r),
6389                }
6390            }
6391            Ok(with_host(|h| h.new_array(out)))
6392        }
6393        "filter" => {
6394            let items = array_items(recv);
6395            let holes = hole_set(recv);
6396            let cb = arg0(&args);
6397            let mut out = Vec::new();
6398            for (i, it) in items.iter().enumerate() {
6399                if holes.contains(&i) {
6400                    continue;
6401                }
6402                let keep = host::invoke(
6403                    &cb,
6404                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6405                    None,
6406                )?;
6407                if with_host(|h| h.truthy(&keep)) {
6408                    out.push(it.clone());
6409                }
6410            }
6411            Ok(with_host(|h| h.new_array(out)))
6412        }
6413        "forEach" => {
6414            let items = array_items(recv);
6415            let holes = hole_set(recv);
6416            let cb = arg0(&args);
6417            for (i, it) in items.iter().enumerate() {
6418                if holes.contains(&i) {
6419                    continue;
6420                }
6421                host::invoke(
6422                    &cb,
6423                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6424                    None,
6425                )?;
6426            }
6427            Ok(Value::Undef)
6428        }
6429        "find" => {
6430            let items = array_items(recv);
6431            let cb = arg0(&args);
6432            for (i, it) in items.iter().enumerate() {
6433                let m = host::invoke(
6434                    &cb,
6435                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6436                    None,
6437                )?;
6438                if with_host(|h| h.truthy(&m)) {
6439                    return Ok(it.clone());
6440                }
6441            }
6442            Ok(Value::Undef)
6443        }
6444        "findIndex" => {
6445            let items = array_items(recv);
6446            let cb = arg0(&args);
6447            for (i, it) in items.iter().enumerate() {
6448                let m = host::invoke(
6449                    &cb,
6450                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6451                    None,
6452                )?;
6453                if with_host(|h| h.truthy(&m)) {
6454                    return Ok(Value::Float(i as f64));
6455                }
6456            }
6457            Ok(Value::Float(-1.0))
6458        }
6459        "some" => {
6460            let items = array_items(recv);
6461            let holes = hole_set(recv);
6462            let cb = arg0(&args);
6463            for (i, it) in items.iter().enumerate() {
6464                if holes.contains(&i) {
6465                    continue;
6466                }
6467                let m = host::invoke(
6468                    &cb,
6469                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6470                    None,
6471                )?;
6472                if with_host(|h| h.truthy(&m)) {
6473                    return Ok(Value::Bool(true));
6474                }
6475            }
6476            Ok(Value::Bool(false))
6477        }
6478        "every" => {
6479            let items = array_items(recv);
6480            let holes = hole_set(recv);
6481            let cb = arg0(&args);
6482            for (i, it) in items.iter().enumerate() {
6483                if holes.contains(&i) {
6484                    continue;
6485                }
6486                let m = host::invoke(
6487                    &cb,
6488                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
6489                    None,
6490                )?;
6491                if !with_host(|h| h.truthy(&m)) {
6492                    return Ok(Value::Bool(false));
6493                }
6494            }
6495            Ok(Value::Bool(true))
6496        }
6497        "reduce" => {
6498            let items = array_items(recv);
6499            let holes = hole_set(recv);
6500            let cb = arg0(&args);
6501            let mut acc;
6502            let mut start = 0;
6503            if args.len() >= 2 {
6504                acc = args[1].clone();
6505            } else {
6506                // With no seed the accumulator is the first PRESENT element, so a
6507                // leading run of holes is skipped rather than seeding `undefined`.
6508                match (0..items.len()).find(|i| !holes.contains(i)) {
6509                    Some(i) => {
6510                        acc = items[i].clone();
6511                        start = i + 1;
6512                    }
6513                    None => {
6514                        return Err(host::type_error(
6515                            "Reduce of empty array with no initial value",
6516                        ))
6517                    }
6518                }
6519            }
6520            for (i, it) in items.iter().enumerate().skip(start) {
6521                if holes.contains(&i) {
6522                    continue;
6523                }
6524                acc = host::invoke(
6525                    &cb,
6526                    vec![acc, it.clone(), Value::Float(i as f64), this_value.clone()],
6527                    None,
6528                )?;
6529            }
6530            Ok(acc)
6531        }
6532        "reduceRight" => {
6533            let items = array_items(recv);
6534            let holes = hole_set(recv);
6535            let cb = arg0(&args);
6536            let n = items.len();
6537            let mut acc;
6538            let mut i = n; // one past the next index to process (walking down)
6539            if args.len() >= 2 {
6540                acc = args[1].clone();
6541            } else {
6542                match (0..n).rev().find(|i| !holes.contains(i)) {
6543                    Some(k) => {
6544                        acc = items[k].clone();
6545                        i = k;
6546                    }
6547                    None => {
6548                        return Err(host::type_error(
6549                            "Reduce of empty array with no initial value",
6550                        ))
6551                    }
6552                }
6553            }
6554            while i > 0 {
6555                i -= 1;
6556                if holes.contains(&i) {
6557                    continue;
6558                }
6559                acc = host::invoke(
6560                    &cb,
6561                    vec![
6562                        acc,
6563                        items[i].clone(),
6564                        Value::Float(i as f64),
6565                        this_value.clone(),
6566                    ],
6567                    None,
6568                )?;
6569            }
6570            Ok(acc)
6571        }
6572        "findLast" => {
6573            let items = array_items(recv);
6574            let cb = arg0(&args);
6575            for i in (0..items.len()).rev() {
6576                let m = host::invoke(
6577                    &cb,
6578                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
6579                    None,
6580                )?;
6581                if with_host(|h| h.truthy(&m)) {
6582                    return Ok(items[i].clone());
6583                }
6584            }
6585            Ok(Value::Undef)
6586        }
6587        "findLastIndex" => {
6588            let items = array_items(recv);
6589            let cb = arg0(&args);
6590            for i in (0..items.len()).rev() {
6591                let m = host::invoke(
6592                    &cb,
6593                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
6594                    None,
6595                )?;
6596                if with_host(|h| h.truthy(&m)) {
6597                    return Ok(Value::Float(i as f64));
6598                }
6599            }
6600            Ok(Value::Float(-1.0))
6601        }
6602        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
6603        // and the holes are re-created at the tail — `[3,,1].sort()` is
6604        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
6605        "sort" => {
6606            let all = array_items(recv);
6607            let holes = hole_set(recv);
6608            let mut items: Vec<Value> = all
6609                .iter()
6610                .enumerate()
6611                .filter(|(i, _)| !holes.contains(i))
6612                .map(|(_, v)| v.clone())
6613                .collect();
6614            sort_values(&mut items, args.first())?;
6615            let present = items.len();
6616            items.resize(all.len(), Value::Undef);
6617            with_host(|h| {
6618                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
6619                    *a = items;
6620                }
6621                h.install_holes(recv, (present..all.len()).collect());
6622            });
6623            Ok(this_value.clone())
6624        }
6625        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
6626        "toSorted" => {
6627            let mut items = array_items(recv);
6628            sort_values(&mut items, args.first())?;
6629            Ok(with_host(|h| h.new_array(items)))
6630        }
6631        "toReversed" => {
6632            let mut items = array_items(recv);
6633            items.reverse();
6634            Ok(with_host(|h| h.new_array(items)))
6635        }
6636        "toSpliced" => {
6637            let mut items = array_items(recv);
6638            let len = items.len();
6639            let start = {
6640                let s = arg_num(&args, 0);
6641                if s < 0.0 {
6642                    ((len as f64 + s).max(0.0)) as usize
6643                } else {
6644                    (s as usize).min(len)
6645                }
6646            };
6647            let delete = if args.len() >= 2 {
6648                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
6649            } else if args.is_empty() {
6650                0
6651            } else {
6652                len - start
6653            };
6654            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
6655            items.splice(start..start + delete, inserts);
6656            Ok(with_host(|h| h.new_array(items)))
6657        }
6658        "with" => {
6659            let mut items = array_items(recv);
6660            let len = items.len() as i64;
6661            let rel = arg_num(&args, 0) as i64;
6662            let idx = if rel < 0 { len + rel } else { rel };
6663            if idx < 0 || idx >= len {
6664                return Err(host::range_error(&format!("Invalid index : {rel}")));
6665            }
6666            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
6667            Ok(with_host(|h| h.new_array(items)))
6668        }
6669        "flat" => {
6670            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
6671            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
6672            let raw = if args.is_empty() {
6673                1.0
6674            } else {
6675                arg_num(&args, 0)
6676            };
6677            let depth = if raw.is_nan() {
6678                0.0
6679            } else if raw.is_infinite() {
6680                raw
6681            } else {
6682                raw.trunc()
6683            };
6684            let mut out = Vec::new();
6685            flatten_into(recv, depth, &mut out)?;
6686            Ok(with_host(|h| h.new_array(out)))
6687        }
6688        "keys" => {
6689            let n = array_len(recv);
6690            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
6691            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6692        }
6693        "values" | "@@iterator" => {
6694            let items = array_items(recv);
6695            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6696        }
6697        "entries" => {
6698            let items = array_items(recv);
6699            let pairs: Vec<Value> = items
6700                .into_iter()
6701                .enumerate()
6702                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
6703                .collect();
6704            Ok(with_host(|h| {
6705                h.alloc(JsObj::Iter {
6706                    items: pairs,
6707                    idx: 0,
6708                })
6709            }))
6710        }
6711        "splice" => array_splice(recv, args),
6712        // `Array.prototype.toString` IS `join()` with the default separator
6713        // (23.1.3.36), so it converts each element with `ToString` too — and
6714        // shares its cycle cut, which is the whole reason it must not call
6715        // `join_parts` directly: `ToString` of a nested array lands back here.
6716        "toString" => join_array(recv, ","),
6717        // An Array inherits from `Object.prototype` too, so the methods it does
6718        // not override resolve there. `[].hasOwnProperty` already read back as a
6719        // function through the property path, but CALLING it landed here and
6720        // threw `is not a function`.
6721        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
6722        _ => Err(host::type_error(&format!("{name} is not a function"))),
6723    }
6724}
6725
6726/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
6727/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
6728/// cut, which is not optional here: `ToString` of an element that is itself an
6729/// array re-enters through `toString`, so guarding only `join` left
6730/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
6731/// process. On node v26.7.0 that expression is `""`.
6732fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
6733    if !host::join_stack_push(recv) {
6734        return Ok(with_host(|h| h.new_str(String::new())));
6735    }
6736    let parts = join_parts(&array_items(recv));
6737    host::join_stack_pop();
6738    let s = parts?.join(sep);
6739    Ok(with_host(|h| h.new_str(s)))
6740}
6741
6742/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
6743/// `null`/`undefined` element contributes the empty string, every other element
6744/// is `ToString(element)` — which for an object means invoking its `toString`,
6745/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
6746/// `"[object Object]"`.
6747///
6748/// The all-primitive array — the overwhelmingly common one — is rendered under
6749/// a single host borrow; only an array actually holding an object pays for the
6750/// re-entrant per-element conversion.
6751fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
6752    let fast = with_host(|h| {
6753        items
6754            .iter()
6755            .map(|x| match x {
6756                Value::Undef => Some(String::new()),
6757                _ if h.is_null(x) => Some(String::new()),
6758                // A SYMBOL element is primitive but has no `ToString`, so it must
6759                // fall through to the fallible path and throw there:
6760                // `[Symbol()].join()` is a TypeError on node v26.7.0.
6761                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
6762                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
6763                _ => None,
6764            })
6765            .collect::<Vec<_>>()
6766    });
6767    if fast.iter().all(Option::is_some) {
6768        return Ok(fast.into_iter().flatten().collect());
6769    }
6770    let mut out = Vec::with_capacity(items.len());
6771    for (x, p) in items.iter().zip(fast) {
6772        match p {
6773            Some(s) => out.push(s),
6774            None => {
6775                let s = host::to_string_value(x)?;
6776                out.push(with_host(|h| h.str_of(&s)));
6777            }
6778        }
6779    }
6780    Ok(out)
6781}
6782
6783/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
6784/// sort — O(n log n) comparisons — with the fallible JS comparator called from
6785/// the merge step; default order is by the string form of each element.
6786/// Propagates a comparator error.
6787///
6788/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
6789/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
6790/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
6791/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
6792/// number of times it is called is.
6793pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
6794    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
6795    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
6796    // invoke path and reporting the generic `null is not a function`.
6797    let cmp = match cmp {
6798        Some(Value::Undef) => None,
6799        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
6800            let shown = with_host(|h| h.inspect(v));
6801            return Err(host::type_error(&format!(
6802                "The comparison function must be either a function or undefined: {shown}"
6803            )));
6804        }
6805        other => other,
6806    };
6807    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
6808    // comparator — it sorts to the end after the defined values are ordered.
6809    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
6810    // node v26.7.0; the insertion sort called the comparator twice, on
6811    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
6812    // is `undefined`, so swapping keeps the defined values in input order.
6813    let mut defined = 0;
6814    for i in 0..items.len() {
6815        if !matches!(items[i], Value::Undef) {
6816            items.swap(defined, i);
6817            defined += 1;
6818        }
6819    }
6820    merge_sort(&mut items[..defined], cmp)
6821}
6822
6823/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
6824/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
6825/// `> 0` and the pair keeps its input order.
6826fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
6827    match cmp {
6828        Some(cb) => {
6829            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
6830            Ok(with_host(|h| h.to_number(&v)))
6831        }
6832        None => {
6833            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
6834            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
6835            // from Rust's `String` order off the BMP.
6836            let x = with_host(|h| h.str_of(a));
6837            let y = with_host(|h| h.str_of(b));
6838            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
6839                Ok(1.0)
6840            } else {
6841                Ok(-1.0)
6842            }
6843        }
6844    }
6845}
6846
6847/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
6848/// array cannot walk the native stack the JS comparator also runs on, and the
6849/// two buffers are swapped each pass instead of copied back.
6850fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
6851    let n = items.len();
6852    if n < 2 {
6853        return Ok(());
6854    }
6855    let mut src = items.to_vec();
6856    let mut dst = src.clone();
6857    let mut width = 1;
6858    while width < n {
6859        let mut lo = 0;
6860        while lo < n {
6861            let mid = (lo + width).min(n);
6862            let hi = (lo + 2 * width).min(n);
6863            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
6864            lo = hi;
6865        }
6866        std::mem::swap(&mut src, &mut dst);
6867        width *= 2;
6868    }
6869    items.clone_from_slice(&src);
6870    Ok(())
6871}
6872
6873/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
6874/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
6875/// keeps the two `k:0` entries in input order, as node does.
6876fn merge(
6877    left: &[Value],
6878    right: &[Value],
6879    out: &mut [Value],
6880    cmp: Option<&Value>,
6881) -> Result<(), String> {
6882    let (mut i, mut j, mut k) = (0, 0, 0);
6883    while i < left.len() && j < right.len() {
6884        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
6885            out[k] = right[j].clone();
6886            j += 1;
6887        } else {
6888            out[k] = left[i].clone();
6889            i += 1;
6890        }
6891        k += 1;
6892    }
6893    for v in left[i..].iter().chain(&right[j..]) {
6894        out[k] = v.clone();
6895        k += 1;
6896    }
6897    Ok(())
6898}
6899
6900/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
6901/// f64 so `Infinity` (full flatten) and finite counts share one path.
6902///
6903/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
6904/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
6905/// exceeded` on node v26.7.0. That is reproduced by checking the same native
6906/// stack floor the VM does, so the answer is a catchable error rather than the
6907/// `fatal runtime error: stack overflow` abort this used to produce.
6908/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
6909/// elements because each level tests `HasProperty` before recursing, so a hole
6910/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
6911fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
6912    if host::stack_exhausted() {
6913        return Err(host::stack_overflow_error());
6914    }
6915    let items = array_items(src);
6916    let holes = hole_set(src);
6917    for (i, it) in items.into_iter().enumerate() {
6918        if holes.contains(&i) {
6919            continue;
6920        }
6921        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
6922        if nested {
6923            flatten_into(&it, depth - 1.0, out)?;
6924        } else {
6925            out.push(it);
6926        }
6927    }
6928    Ok(())
6929}
6930
6931fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
6932    let len = array_len(recv);
6933    let start = {
6934        let s = arg_num(&args, 0);
6935        if s < 0.0 {
6936            ((len as f64 + s).max(0.0)) as usize
6937        } else {
6938            (s as usize).min(len)
6939        }
6940    };
6941    let delete = if args.len() >= 2 {
6942        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
6943    } else {
6944        len - start
6945    };
6946    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
6947    let inserted = inserts.len();
6948    // The receiver's holes shift by (inserted - deleted) past the cut, and the
6949    // ones inside the cut move into the RETURNED array at their offset there.
6950    let holes = hole_set(recv);
6951    let removed = with_host(|h| {
6952        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
6953            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
6954            removed
6955        } else {
6956            Vec::new()
6957        }
6958    });
6959    Ok(with_host(|h| {
6960        h.install_holes(
6961            recv,
6962            holes
6963                .iter()
6964                .filter_map(|&i| {
6965                    if i < start {
6966                        Some(i)
6967                    } else if i < start + delete {
6968                        None
6969                    } else {
6970                        Some(i - delete + inserted)
6971                    }
6972                })
6973                .collect(),
6974        );
6975        let out = h.new_array(removed);
6976        h.install_holes(
6977            &out,
6978            holes
6979                .iter()
6980                .filter(|&&i| i >= start && i < start + delete)
6981                .map(|&i| i - start)
6982                .collect(),
6983        );
6984        out
6985    }))
6986}
6987
6988fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
6989    let norm = |v: f64| -> usize {
6990        if v < 0.0 {
6991            ((len as f64 + v).max(0.0)) as usize
6992        } else {
6993            (v as usize).min(len)
6994        }
6995    };
6996    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
6997        0
6998    } else {
6999        norm(arg_num(args, 0))
7000    };
7001    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
7002        len
7003    } else {
7004        norm(arg_num(args, 1))
7005    };
7006    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
7007    // never a reversed one: JS `slice` clamps `end` up to `start`.
7008    (lo, hi.max(lo))
7009}
7010
7011fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
7012    // Every index-bearing method below counts UTF-16 code units, so they all
7013    // work off this one decoding rather than off `s.chars()` (code points),
7014    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
7015    let u = crate::utf16::Units::of(s);
7016    match name {
7017        // `for…of` / spread over a string iterates CODE POINTS, not code units:
7018        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
7019        // is the one string operation that is specified in chars, so it stays
7020        // on `s.chars()` on purpose — do not "fix" it to match the others.
7021        "@@iterator" => {
7022            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
7023            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
7024        }
7025        "toUpperCase" => Ok(new_s(s.to_uppercase())),
7026        "toLowerCase" => Ok(new_s(s.to_lowercase())),
7027        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
7028        // from the plain forms only for the locale-specific mappings (Turkish
7029        // dotless i, Lithuanian accents); with no locale argument they are the
7030        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
7031        // `to_lowercase`. They threw `is not a function` before, so the common
7032        // no-argument call — the only form this runtime can answer, since it
7033        // carries no ICU — failed outright rather than agreeing with node.
7034        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
7035        // is `'i'` here and `'ı'` in node.
7036        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
7037        // has no locale rendering. Missing it made an ARRAY of strings fail too,
7038        // since `Array.prototype.toLocaleString` invokes it per element.
7039        "toLocaleString" => Ok(new_s(s.to_string())),
7040        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
7041        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
7042        // Locale comparison (ASCII approximation of ICU collation): primary by
7043        // case-folded order, then lowercase sorts before uppercase at a tie.
7044        "localeCompare" => {
7045            let other = with_host(|h| h.str_of(&arg0(&args)));
7046            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
7047            let r = match la.cmp(&lb) {
7048                std::cmp::Ordering::Less => -1.0,
7049                std::cmp::Ordering::Greater => 1.0,
7050                std::cmp::Ordering::Equal => {
7051                    let mut t = 0.0;
7052                    for (ca, cb) in s.chars().zip(other.chars()) {
7053                        if ca != cb {
7054                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
7055                            break;
7056                        }
7057                    }
7058                    t
7059                }
7060            };
7061            Ok(Value::Float(r))
7062        }
7063        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
7064        //
7065        // This used to return the receiver unchanged and only validate the FORM
7066        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
7067        // one code point) and `"Å"` (NFD, two) stayed distinct under
7068        // `.normalize()`, so the standard way to compare Unicode text for
7069        // canonical equivalence silently answered `false`, and `NFKC` never
7070        // folded a compatibility character (`"fi"` stayed one code point instead
7071        // of becoming `"fi"`). The tables come from `unicode-normalization`.
7072        "normalize" => {
7073            use unicode_normalization::UnicodeNormalization;
7074            let form = match args.first() {
7075                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
7076                _ => "NFC".to_string(),
7077            };
7078            let out = match form.as_str() {
7079                "NFC" => s.nfc().collect::<String>(),
7080                "NFD" => s.nfd().collect::<String>(),
7081                "NFKC" => s.nfkc().collect::<String>(),
7082                "NFKD" => s.nfkd().collect::<String>(),
7083                _ => {
7084                    return Err(host::range_error(
7085                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
7086                    ))
7087                }
7088            };
7089            Ok(new_s(out))
7090        }
7091        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
7092        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
7093        // value this runtime can hold is well-formed by construction and
7094        // `toWellFormed` has nothing to replace. Both answers are therefore
7095        // exact for every string that survives storage; the one case node
7096        // answers differently is a surrogate half extracted by `charAt`/`slice`,
7097        // which is already `U+FFFD` here — the documented lone-surrogate
7098        // boundary in `utf16`, not a separate gap.
7099        "isWellFormed" => Ok(Value::Bool(true)),
7100        "toWellFormed" => Ok(new_s(s.to_string())),
7101        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
7102        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
7103        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
7104        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
7105        "toString" | "valueOf" => Ok(new_s(s.to_string())),
7106        "charAt" => {
7107            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
7108            Ok(new_s(at.unwrap_or_default()))
7109        }
7110        "at" => {
7111            let n = arg_num(&args, 0);
7112            // A negative position counts back from the end; `NaN` is 0. An
7113            // infinite position is out of range in either direction.
7114            let i = if n.is_nan() {
7115                Some(0i64)
7116            } else if n.is_finite() {
7117                let i = n.trunc() as i64;
7118                Some(if i < 0 { i + u.len() as i64 } else { i })
7119            } else {
7120                None
7121            };
7122            match i
7123                .and_then(|i| usize::try_from(i).ok())
7124                .and_then(|i| u.unit_str(i))
7125            {
7126                Some(c) => Ok(new_s(c)),
7127                None => Ok(Value::Undef),
7128            }
7129        }
7130        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
7131        // astral character, not the character. `codePointAt` looks ahead one
7132        // unit and reports the whole scalar when the pair is well formed. They
7133        // agree everywhere on the BMP, which is why they used to share an arm.
7134        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
7135        // `codePointAt` yields `undefined` (measured on node v26.7.0).
7136        "charCodeAt" => {
7137            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
7138            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
7139        }
7140        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
7141            Some(cp) => Ok(Value::Float(f64::from(cp))),
7142            None => Ok(Value::Undef),
7143        },
7144        // The search quartet all honor their optional position argument.
7145        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
7146        // parameterCount walks a query string with exactly that call.
7147        "indexOf" => {
7148            let needle = needle_units(&args);
7149            let from = clamp_pos(arg_num(&args, 1), u.len());
7150            Ok(Value::Float(
7151                search_from(u.as_slice(), needle.as_slice(), from)
7152                    .map(|i| i as f64)
7153                    .unwrap_or(-1.0),
7154            ))
7155        }
7156        "lastIndexOf" => {
7157            let needle = needle_units(&args);
7158            // An absent or NaN position means "search the whole string".
7159            let n = arg_num(&args, 1);
7160            let upto = if n.is_nan() {
7161                u.len()
7162            } else {
7163                clamp_pos(n, u.len())
7164            };
7165            Ok(Value::Float(
7166                search_last(u.as_slice(), needle.as_slice(), upto)
7167                    .map(|i| i as f64)
7168                    .unwrap_or(-1.0),
7169            ))
7170        }
7171        "includes" => {
7172            let needle = needle_units(&args);
7173            let from = clamp_pos(arg_num(&args, 1), u.len());
7174            Ok(Value::Bool(
7175                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
7176            ))
7177        }
7178        "startsWith" => {
7179            let needle = needle_units(&args);
7180            let from = clamp_pos(arg_num(&args, 1), u.len());
7181            Ok(Value::Bool(
7182                u.as_slice()[from..].starts_with(needle.as_slice()),
7183            ))
7184        }
7185        "endsWith" => {
7186            let needle = needle_units(&args);
7187            // The 2nd argument is where the string is treated as ENDING.
7188            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
7189                u.len()
7190            } else {
7191                clamp_pos(arg_num(&args, 1), u.len())
7192            };
7193            Ok(Value::Bool(
7194                u.as_slice()[..end].ends_with(needle.as_slice()),
7195            ))
7196        }
7197        "slice" => {
7198            let (lo, hi) = slice_bounds(&args, u.len());
7199            Ok(new_s(u.slice(lo, hi)))
7200        }
7201        "substring" => {
7202            let mut a = arg_num(&args, 0).max(0.0) as usize;
7203            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
7204                u.len()
7205            } else {
7206                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
7207            };
7208            a = a.min(u.len());
7209            if a > b {
7210                std::mem::swap(&mut a, &mut b);
7211            }
7212            Ok(new_s(u.slice(a, b)))
7213        }
7214        "substr" => {
7215            // A negative start counts from the end: max(len + start, 0).
7216            let len = u.len() as i64;
7217            let mut start = arg_num(&args, 0) as i64;
7218            if start < 0 {
7219                start = (len + start).max(0);
7220            }
7221            let start = (start as usize).min(u.len());
7222            let count = if args.len() >= 2 {
7223                arg_num(&args, 1).max(0.0) as usize
7224            } else {
7225                u.len()
7226            };
7227            let end = start.saturating_add(count).min(u.len());
7228            Ok(new_s(u.slice(start, end)))
7229        }
7230        "repeat" => {
7231            let n = arg_num(&args, 0);
7232            // `RangeError`, not `TypeError`, and the count is named:
7233            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
7234            if n < 0.0 || !n.is_finite() {
7235                return Err(host::range_error(&format!(
7236                    "Invalid count value: {}",
7237                    host::fmt_number(n)
7238                )));
7239            }
7240            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
7241            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
7242            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
7243            // is `RangeError: Invalid string length`.
7244            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
7245                return Err(host::invalid_string_length());
7246            }
7247            Ok(new_s(s.repeat(n as usize)))
7248        }
7249        "concat" => {
7250            let mut out = s.to_string();
7251            for a in &args {
7252                out.push_str(&with_host(|h| h.str_of(a)));
7253            }
7254            Ok(new_s(out))
7255        }
7256        "padStart" => Ok(new_s(pad(s, &args, true)?)),
7257        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
7258        // Regex-taking string methods: dispatch to the regexp module when the
7259        // argument is a RegExp; otherwise keep the plain-string behavior.
7260        "match" => crate::regexp::str_match(s, &arg0(&args)),
7261        "matchAll" => crate::regexp::str_match_all(s, &arg0(&args)),
7262        "search" => {
7263            if is_regexp_arg(&arg0(&args)) {
7264                crate::regexp::str_search(s, &arg0(&args))
7265            } else {
7266                // A string arg is coerced to a (literal) regex; we approximate with
7267                // a plain substring search, which agrees for non-metacharacter
7268                // needles.
7269                let needle = with_host(|h| h.str_of(&arg0(&args)));
7270                Ok(Value::Float(byte_to_unit_index(s, s.find(&needle))))
7271            }
7272        }
7273        "replace" => {
7274            let pat = arg0(&args);
7275            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
7276            if is_regexp_arg(&pat) {
7277                crate::regexp::str_replace_regex(s, &pat, &repl, false)
7278            } else if with_host(|h| host::is_callable(h, &repl)) {
7279                Ok(new_s(replace_str_fn(
7280                    s,
7281                    &with_host(|h| h.str_of(&pat)),
7282                    &repl,
7283                    false,
7284                )?))
7285            } else {
7286                let from = with_host(|h| h.str_of(&pat));
7287                let to = with_host(|h| h.str_of(&repl));
7288                Ok(new_s(s.replacen(&from, &to, 1)))
7289            }
7290        }
7291        "replaceAll" => {
7292            let pat = arg0(&args);
7293            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
7294            if is_regexp_arg(&pat) {
7295                crate::regexp::str_replace_regex(s, &pat, &repl, true)
7296            } else if with_host(|h| host::is_callable(h, &repl)) {
7297                Ok(new_s(replace_str_fn(
7298                    s,
7299                    &with_host(|h| h.str_of(&pat)),
7300                    &repl,
7301                    true,
7302                )?))
7303            } else {
7304                let from = with_host(|h| h.str_of(&pat));
7305                let to = with_host(|h| h.str_of(&repl));
7306                Ok(new_s(s.replace(&from, &to)))
7307            }
7308        }
7309        "split" => {
7310            if is_regexp_arg(&arg0(&args)) {
7311                let limit = args
7312                    .get(1)
7313                    .filter(|v| !matches!(v, Value::Undef))
7314                    .map(|v| with_host(|h| h.to_number(v)) as usize);
7315                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
7316            }
7317            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
7318                vec![new_s(s.to_string())]
7319            } else {
7320                let sep = with_host(|h| h.str_of(&args[0]));
7321                if sep.is_empty() {
7322                    // `split('')` yields one element per code UNIT, so an astral
7323                    // character becomes its two surrogate halves.
7324                    (0..u.len())
7325                        .filter_map(|i| u.unit_str(i))
7326                        .map(new_s)
7327                        .collect()
7328                } else {
7329                    s.split(&sep as &str)
7330                        .map(|p| new_s(p.to_string()))
7331                        .collect()
7332                }
7333            };
7334            // Optional limit: keep at most `limit` substrings.
7335            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
7336                let n = with_host(|h| h.to_number(lim));
7337                if n.is_finite() && n >= 0.0 {
7338                    parts.truncate(n as usize);
7339                }
7340            }
7341            Ok(with_host(|h| h.new_array(parts)))
7342        }
7343        _ => Err(host::type_error(&format!("{name} is not a function"))),
7344    }
7345}
7346
7347fn new_s(s: String) -> Value {
7348    with_host(|h| h.new_str(s))
7349}
7350
7351/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
7352/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
7353fn clamp_pos(n: f64, len: usize) -> usize {
7354    if n.is_nan() || n <= 0.0 {
7355        0
7356    } else if n >= len as f64 {
7357        len
7358    } else {
7359        n.trunc() as usize
7360    }
7361}
7362
7363/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
7364/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
7365/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
7366fn unit_pos(n: f64) -> Option<usize> {
7367    if n.is_nan() {
7368        Some(0)
7369    } else if n < 0.0 || !n.is_finite() {
7370        None
7371    } else {
7372        Some(n.trunc() as usize)
7373    }
7374}
7375
7376/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
7377/// the needle is compared in the same alphabet the haystack is indexed by.
7378fn needle_units(args: &[Value]) -> crate::utf16::Units {
7379    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
7380}
7381
7382/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
7383/// needle matches at `from` itself, as JS specifies.
7384fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
7385    if needle.is_empty() {
7386        return Some(from.min(hay.len()));
7387    }
7388    if needle.len() > hay.len() {
7389        return None;
7390    }
7391    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
7392}
7393
7394/// The highest index `<= upto` at which `needle` occurs in `hay`.
7395fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
7396    if needle.is_empty() {
7397        return Some(upto.min(hay.len()));
7398    }
7399    if needle.len() > hay.len() {
7400        return None;
7401    }
7402    let last = hay.len() - needle.len();
7403    (0..=upto.min(last))
7404        .rev()
7405        .find(|&i| &hay[i..i + needle.len()] == needle)
7406}
7407
7408/// A UTF-8 byte offset reported back to JS as a string position — a UTF-16
7409/// code-unit index — or `-1` for "not found".
7410fn byte_to_unit_index(s: &str, byte: Option<usize>) -> f64 {
7411    match byte {
7412        Some(b) => crate::utf16::index_of_byte(s, b).get() as f64,
7413        None => -1.0,
7414    }
7415}
7416
7417fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
7418    let target_f = arg_num(args, 0);
7419    let target = if target_f.is_finite() && target_f > 0.0 {
7420        target_f as usize
7421    } else {
7422        0
7423    };
7424    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
7425    // is `'-𝒳'` in node, not `'--𝒳'`.
7426    let cur = crate::utf16::len(s);
7427    if cur >= target {
7428        return Ok(s.to_string());
7429    }
7430    let filler = if args.len() >= 2 {
7431        with_host(|h| h.str_of(&args[1]))
7432    } else {
7433        " ".to_string()
7434    };
7435    if filler.is_empty() {
7436        return Ok(s.to_string());
7437    }
7438    // Checked only AFTER the two short-circuits, which is the order V8 uses:
7439    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
7440    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
7441    if target_f > host::MAX_STRING_LENGTH as f64 {
7442        return Err(host::invalid_string_length());
7443    }
7444    let need = target - cur;
7445    let fill = crate::utf16::Units::of(&filler);
7446    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
7447    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
7448    // (see src/utf16.rs).
7449    let units: Vec<u16> = (0..need)
7450        .filter_map(|i| fill.unit(i % fill.len()))
7451        .collect();
7452    let padding = crate::utf16::to_string_lossy(&units);
7453    Ok(if start {
7454        format!("{padding}{s}")
7455    } else {
7456        format!("{s}{padding}")
7457    })
7458}
7459
7460/// V8's radix rejection, shared by `Number.prototype.toString` and
7461/// `BigInt.prototype.toString` — one string, because they are one message and
7462/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
7463/// must be").
7464const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
7465
7466/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
7467fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
7468    match name {
7469        "toString" => {
7470            let radix = match args.first() {
7471                None | Some(Value::Undef) => 10,
7472                Some(_) => {
7473                    let t = arg_num(&args, 0).trunc();
7474                    if !(2.0..=36.0).contains(&t) {
7475                        return Err(host::range_error(RADIX_RANGE));
7476                    }
7477                    t as u32
7478                }
7479            };
7480            Ok(new_s(b.to_str_radix(radix)))
7481        }
7482        // `BigInt.prototype.toLocaleString` groups thousands like the Number
7483        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
7484        // returning the bare digits made it the only numeric type that skipped
7485        // grouping. Same en-US-shaped output as `Number.prototype`; the
7486        // `locales`/`options` arguments are ignored (no ICU here).
7487        "toLocaleString" => {
7488            let digits = b.magnitude().to_string();
7489            let sign = if b.sign() == num_bigint::Sign::Minus {
7490                "-"
7491            } else {
7492                ""
7493            };
7494            Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
7495        }
7496        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
7497        _ => Err(host::type_error(&format!("{name} is not a function"))),
7498    }
7499}
7500
7501fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
7502    match name {
7503        "toFixed" => {
7504            let digits = arg_num(&args, 0);
7505            if !(0.0..=100.0).contains(&digits.trunc()) {
7506                return Err(host::range_error(
7507                    "toFixed() digits argument must be between 0 and 100",
7508                ));
7509            }
7510            Ok(new_s(to_fixed(n, digits as usize)))
7511        }
7512        "toExponential" => {
7513            // `undefined` (or a missing argument) selects the shortest form.
7514            let f = match args.first() {
7515                None | Some(Value::Undef) => None,
7516                Some(_) => {
7517                    let d = arg_num(&args, 0).trunc();
7518                    if !(0.0..=100.0).contains(&d) {
7519                        return Err(host::range_error(
7520                            "toExponential() argument must be between 0 and 100",
7521                        ));
7522                    }
7523                    Some(d as usize)
7524                }
7525            };
7526            Ok(new_s(to_exponential(n, f)))
7527        }
7528        "toString" => {
7529            // An out-of-range radix THROWS; it does not silently fall back to
7530            // base 10. `(1).toString(37)` returned "1" here, so a support probe
7531            // was told every radix worked.
7532            let radix = match args.first() {
7533                None | Some(Value::Undef) => 10,
7534                Some(_) => {
7535                    let r = arg_num(&args, 0);
7536                    let t = r.trunc();
7537                    if !(2.0..=36.0).contains(&t) {
7538                        return Err(host::range_error(RADIX_RANGE));
7539                    }
7540                    t as u32
7541                }
7542            };
7543            if radix == 10 {
7544                Ok(new_s(host::fmt_number(n)))
7545            } else {
7546                Ok(new_s(to_radix(n, radix)))
7547            }
7548        }
7549        "toPrecision" => {
7550            // `undefined` (or a missing argument) behaves like `toString()`.
7551            match args.first() {
7552                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
7553                Some(_) => {
7554                    let p = arg_num(&args, 0).trunc();
7555                    if !(1.0..=100.0).contains(&p) {
7556                        return Err(host::range_error(
7557                            "toPrecision() argument must be between 1 and 100",
7558                        ));
7559                    }
7560                    Ok(new_s(to_precision(n, p as usize)))
7561                }
7562            }
7563        }
7564        "toLocaleString" => Ok(new_s(to_locale_string(n))),
7565        "valueOf" => Ok(Value::Float(n)),
7566        _ => Err(host::type_error(&format!("{name} is not a function"))),
7567    }
7568}
7569
7570/// `Number.prototype.toLocaleString()` with the default locale and options:
7571/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
7572/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
7573/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
7574/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
7575/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
7576fn to_locale_string(n: f64) -> String {
7577    if n.is_nan() {
7578        return "NaN".to_string();
7579    }
7580    if n.is_infinite() {
7581        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
7582    }
7583    let neg = n.is_sign_negative();
7584    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
7585    // (and a bare trailing point). `to_fixed` rounds half away from zero.
7586    // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
7587    // which is exponential — and the grouping below then chopped up the
7588    // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
7589    // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
7590    // source: node groups the shortest decimal form, so `(1e100)
7591    // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
7592    // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
7593    // deliberately different rule — see `bigint_ctor`.)
7594    let fixed = expand_exponential(&to_fixed(n.abs(), 3));
7595    let trimmed = match fixed.split_once('.') {
7596        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
7597        None => fixed.as_str(),
7598    };
7599    let (int_part, frac_part) = match trimmed.split_once('.') {
7600        Some((i, f)) => (i, Some(f)),
7601        None => (trimmed, None),
7602    };
7603    let mut out = String::new();
7604    if neg {
7605        out.push('-'); // Intl keeps the sign even for -0.
7606    }
7607    out.push_str(&group_thousands(int_part));
7608    if let Some(f) = frac_part {
7609        out.push('.');
7610        out.push_str(f);
7611    }
7612    out
7613}
7614
7615/// Write a nonnegative decimal string in plain positional form, expanding an
7616/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
7617/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
7618/// negative exponent (a magnitude below 1, which the caller has already rounded
7619/// to zero), is returned unchanged.
7620fn expand_exponential(s: &str) -> String {
7621    let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
7622        return s.to_string();
7623    };
7624    let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
7625        return s.to_string();
7626    };
7627    if exp <= 0 {
7628        return s.to_string();
7629    }
7630    let (int_digits, frac_digits) = match mantissa.split_once('.') {
7631        Some((i, f)) => (i.to_string(), f.to_string()),
7632        None => (mantissa.to_string(), String::new()),
7633    };
7634    let mut digits = int_digits;
7635    digits.push_str(&frac_digits);
7636    // The exponent consumes the fractional digits first; whatever is left
7637    // becomes trailing zeros.
7638    let zeros = exp as usize - frac_digits.len().min(exp as usize);
7639    digits.push_str(&"0".repeat(zeros));
7640    digits
7641}
7642
7643/// Insert `,` as a thousands separator into a nonnegative integer digit string.
7644fn group_thousands(int_part: &str) -> String {
7645    let bytes = int_part.as_bytes();
7646    let n = bytes.len();
7647    let mut out = String::with_capacity(n + n / 3);
7648    for (i, &b) in bytes.iter().enumerate() {
7649        if i > 0 && (n - i) % 3 == 0 {
7650            out.push(',');
7651        }
7652        out.push(b as char);
7653    }
7654    out
7655}
7656
7657/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
7658/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
7659/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
7660/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
7661///
7662/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
7663/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
7664/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
7665fn to_fixed(n: f64, f: usize) -> String {
7666    if !n.is_finite() {
7667        return host::fmt_number(n);
7668    }
7669    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
7670    if n.abs() >= 1e21 {
7671        return host::fmt_number(n);
7672    }
7673    let neg = n < 0.0;
7674    // Exact decimal with guard digits past the rounding position; then round the
7675    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
7676    let full = format!("{:.*}", f + 25, n.abs());
7677    let mut body = round_decimal_string(&full, f);
7678    if neg {
7679        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
7680    }
7681    body
7682}
7683
7684/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
7685/// fractional digits, half away from zero, propagating carry across the point.
7686fn round_decimal_string(s: &str, f: usize) -> String {
7687    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
7688    let mut digits: Vec<u8> = int_part
7689        .bytes()
7690        .chain(frac_part.bytes())
7691        .map(|b| b - b'0')
7692        .collect();
7693    let point = int_part.len(); // digits before the decimal point
7694    let keep = point + f; // number of leading digits to keep
7695
7696    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
7697    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
7698        let mut i = keep;
7699        loop {
7700            if i == 0 {
7701                digits.insert(0, 1);
7702                // A new leading digit shifts the decimal point right by one.
7703                return assemble_decimal(&digits, point + 1, f);
7704            }
7705            i -= 1;
7706            if digits[i] == 9 {
7707                digits[i] = 0;
7708            } else {
7709                digits[i] += 1;
7710                break;
7711            }
7712        }
7713    }
7714    assemble_decimal(&digits, point, f)
7715}
7716
7717/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
7718/// `point` digits precede the decimal point.
7719fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
7720    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
7721    let int_str = int_str.trim_start_matches('0');
7722    let int_str = if int_str.is_empty() { "0" } else { int_str };
7723    if f == 0 {
7724        return int_str.to_string();
7725    }
7726    let frac: String = digits[point..point + f]
7727        .iter()
7728        .map(|d| (d + b'0') as char)
7729        .collect();
7730    format!("{int_str}.{frac}")
7731}
7732
7733/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
7734/// from zero, returning the `p` digits and the decimal exponent `e` such that the
7735/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
7736/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
7737/// exact digits are taken with guard positions and rounded here.
7738fn round_significant(a: f64, p: usize) -> (String, i32) {
7739    let sci = format!("{a:.*e}", p - 1 + 25);
7740    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
7741    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
7742    let all: Vec<u8> = mant
7743        .chars()
7744        .filter(|c| c.is_ascii_digit())
7745        .map(|c| c as u8 - b'0')
7746        .collect();
7747    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
7748    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
7749        // Round the p-digit mantissa up, propagating carry; a carry out of the
7750        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
7751        let mut d: Vec<u8> = all[..p].to_vec();
7752        let mut i = p;
7753        loop {
7754            if i == 0 {
7755                d.insert(0, 1);
7756                d.truncate(p);
7757                e += 1;
7758                break;
7759            }
7760            i -= 1;
7761            if d[i] == 9 {
7762                d[i] = 0;
7763            } else {
7764                d[i] += 1;
7765                break;
7766            }
7767        }
7768        s = d.iter().map(|x| (x + b'0') as char).collect();
7769    }
7770    (s, e)
7771}
7772
7773/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
7774/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
7775/// `f` omitted, as many digits as uniquely identify the value are used
7776/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
7777/// on the exact value, matching `toPrecision`.
7778fn to_exponential(n: f64, f: Option<usize>) -> String {
7779    if !n.is_finite() {
7780        return host::fmt_number(n);
7781    }
7782    let neg = n < 0.0;
7783    let a = n.abs();
7784    let (s, e) = if a == 0.0 {
7785        // Zero has no significant digits: emit "0" padded to the requested width.
7786        ("0".repeat(f.unwrap_or(0) + 1), 0)
7787    } else {
7788        match f {
7789            Some(f) => round_significant(a, f + 1),
7790            None => {
7791                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
7792                let sci = format!("{a:e}");
7793                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
7794                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
7795                let trimmed = digits.trim_end_matches('0');
7796                let digits = if trimmed.is_empty() { "0" } else { trimmed };
7797                (digits.to_string(), exp_str.parse().unwrap_or(0))
7798            }
7799        }
7800    };
7801    let sign = if e >= 0 { '+' } else { '-' };
7802    let mag = e.abs();
7803    let body = if s.len() == 1 {
7804        format!("{s}e{sign}{mag}")
7805    } else {
7806        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
7807    };
7808    if neg {
7809        format!("-{body}")
7810    } else {
7811        body
7812    }
7813}
7814
7815/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
7816/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
7817/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
7818/// retained (`(100).toPrecision(5) === "100.00"`).
7819fn to_precision(n: f64, p: usize) -> String {
7820    if !n.is_finite() {
7821        return host::fmt_number(n);
7822    }
7823    if n == 0.0 {
7824        return if p == 1 {
7825            "0".into()
7826        } else {
7827            format!("0.{}", "0".repeat(p - 1))
7828        };
7829    }
7830    let neg = n < 0.0;
7831    let (s, e) = round_significant(n.abs(), p);
7832    let pp = p as i32;
7833
7834    let body = if e < -6 || e >= pp {
7835        // Exponential: first digit, optional '.rest', signed exponent.
7836        let sign = if e >= 0 { '+' } else { '-' };
7837        let mag = e.abs();
7838        if p == 1 {
7839            format!("{s}e{sign}{mag}")
7840        } else {
7841            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
7842        }
7843    } else if e >= 0 {
7844        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
7845        let ip = (e + 1) as usize;
7846        if ip == p {
7847            s
7848        } else {
7849            format!("{}.{}", &s[..ip], &s[ip..])
7850        }
7851    } else {
7852        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
7853        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
7854    };
7855    if neg {
7856        format!("-{body}")
7857    } else {
7858        body
7859    }
7860}
7861
7862/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
7863/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
7864/// is emitted exact, and fractional digits are produced up to the input double's
7865/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
7866/// carry-over back into already-written digits (and into the integer part).
7867fn to_radix(n: f64, radix: u32) -> String {
7868    if !n.is_finite() {
7869        return host::fmt_number(n);
7870    }
7871    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
7872    let rf = radix as f64;
7873    let neg = n < 0.0;
7874    let value = n.abs();
7875
7876    let mut integer = value.floor();
7877    let mut fraction = value - integer;
7878
7879    // Fraction digits, most-significant first.
7880    let mut frac: Vec<u8> = Vec::new();
7881    // Only compute fractional digits down to the input double's precision.
7882    let mut delta = 0.5 * (next_up(value) - value);
7883    delta = delta.max(next_up(0.0));
7884    if fraction >= delta {
7885        loop {
7886            // Shift up by one digit.
7887            fraction *= rf;
7888            delta *= rf;
7889            let digit = fraction as usize;
7890            frac.push(digits[digit]);
7891            fraction -= digit as f64;
7892            // Round to even.
7893            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
7894                // Carry-over: back-trace already-written fraction digits.
7895                loop {
7896                    match frac.pop() {
7897                        None => {
7898                            // Carried past the point into the integer part.
7899                            integer += 1.0;
7900                            break;
7901                        }
7902                        Some(c) => {
7903                            let d = if c > b'9' {
7904                                (c - b'a' + 10) as u32
7905                            } else {
7906                                (c - b'0') as u32
7907                            };
7908                            if d + 1 < radix {
7909                                frac.push(digits[(d + 1) as usize]);
7910                                break;
7911                            }
7912                            // digit was radix-1: drop it and keep carrying.
7913                        }
7914                    }
7915                }
7916                break;
7917            }
7918            if fraction < delta {
7919                break;
7920            }
7921        }
7922    }
7923
7924    // Integer digits, least-significant first (reversed at the end).
7925    let mut int_out: Vec<u8> = Vec::new();
7926    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
7927    while v8_exponent(integer / rf) > 0 {
7928        integer /= rf;
7929        int_out.push(b'0');
7930    }
7931    loop {
7932        let remainder = integer % rf;
7933        int_out.push(digits[remainder as usize]);
7934        integer = (integer - remainder) / rf;
7935        if integer <= 0.0 {
7936            break;
7937        }
7938    }
7939    int_out.reverse();
7940
7941    let mut out: Vec<u8> = Vec::new();
7942    if neg {
7943        out.push(b'-');
7944    }
7945    out.extend_from_slice(&int_out);
7946    if !frac.is_empty() {
7947        out.push(b'.');
7948        out.extend_from_slice(&frac);
7949    }
7950    String::from_utf8(out).unwrap()
7951}
7952
7953/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
7954fn next_up(x: f64) -> f64 {
7955    f64::from_bits(x.to_bits() + 1)
7956}
7957
7958/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
7959/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
7960fn v8_exponent(x: f64) -> i32 {
7961    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
7962    if biased == 0 {
7963        -1074 // denormal
7964    } else {
7965        biased - 1075
7966    }
7967}
7968
7969// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
7970
7971/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
7972/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
7973/// but the value kept alongside it is what iteration and `console.log` report,
7974/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
7975fn normalize_zero_key(v: Value) -> Value {
7976    match v {
7977        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
7978        other => other,
7979    }
7980}
7981
7982fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
7983    match name {
7984        "get" => {
7985            let key = with_host(|h| host::map_key(h, &arg0(&args)));
7986            Ok(with_host(|h| match h.get(recv) {
7987                Some(JsObj::Map { entries, .. }) => entries
7988                    .get(&key)
7989                    .map(|(_, v)| v.clone())
7990                    .unwrap_or(Value::Undef),
7991                _ => Value::Undef,
7992            }))
7993        }
7994        "set" => {
7995            let kv = normalize_zero_key(arg0(&args));
7996            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
7997            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
7998            let key = with_host(|h| host::map_key(h, &kv));
7999            with_host(|h| {
8000                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
8001                    entries.insert(key, (kv, vv));
8002                }
8003            });
8004            Ok(recv.clone())
8005        }
8006        "has" => {
8007            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8008            Ok(Value::Bool(with_host(
8009                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
8010            )))
8011        }
8012        "delete" => {
8013            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8014            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
8015                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
8016                _ => false,
8017            })))
8018        }
8019        "clear" => {
8020            with_host(|h| {
8021                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
8022                    entries.clear();
8023                }
8024            });
8025            Ok(Value::Undef)
8026        }
8027        "forEach" => {
8028            let cb = arg0(&args);
8029            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
8030                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
8031                _ => Vec::new(),
8032            });
8033            for (k, v) in pairs {
8034                host::invoke(&cb, vec![v, k, recv.clone()], None)?;
8035            }
8036            Ok(Value::Undef)
8037        }
8038        "keys" | "values" | "entries" | "@@iterator" => {
8039            let items: Vec<Value> = with_host(|h| {
8040                let pairs: Vec<(Value, Value)> = match h.get(recv) {
8041                    Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
8042                    _ => Vec::new(),
8043                };
8044                pairs
8045                    .into_iter()
8046                    .map(|(k, v)| match name {
8047                        "keys" => k,
8048                        "values" => v,
8049                        _ => h.new_array(vec![k, v]), // entries + @@iterator
8050                    })
8051                    .collect()
8052            });
8053            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8054        }
8055        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
8056    }
8057}
8058
8059/// A weak collection can only hold objects (and unregistered symbols) — a
8060/// primitive key is a `TypeError`, which is how packages probe for weak support.
8061fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
8062    let weak = with_host(|h| {
8063        matches!(
8064            h.get(recv),
8065            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
8066        )
8067    });
8068    if !weak {
8069        return Ok(());
8070    }
8071    let is_object = with_host(|h| match key {
8072        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
8073        _ => false,
8074    });
8075    if is_object {
8076        return Ok(());
8077    }
8078    Err(host::type_error(if kind == "WeakMap" {
8079        "Invalid value used as weak map key"
8080    } else {
8081        "Invalid value used in weak set"
8082    }))
8083}
8084
8085fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8086    match name {
8087        "add" => {
8088            let vv = normalize_zero_key(arg0(&args));
8089            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
8090            let key = with_host(|h| host::map_key(h, &vv));
8091            with_host(|h| {
8092                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
8093                    entries.insert(key, vv);
8094                }
8095            });
8096            Ok(recv.clone())
8097        }
8098        "has" => {
8099            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8100            Ok(Value::Bool(with_host(
8101                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
8102            )))
8103        }
8104        "delete" => {
8105            let key = with_host(|h| host::map_key(h, &arg0(&args)));
8106            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
8107                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
8108                _ => false,
8109            })))
8110        }
8111        "clear" => {
8112            with_host(|h| {
8113                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
8114                    entries.clear();
8115                }
8116            });
8117            Ok(Value::Undef)
8118        }
8119        "forEach" => {
8120            let cb = arg0(&args);
8121            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
8122                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
8123                _ => Vec::new(),
8124            });
8125            for v in vals {
8126                host::invoke(&cb, vec![v.clone(), v, recv.clone()], None)?;
8127            }
8128            Ok(Value::Undef)
8129        }
8130        "keys" | "values" | "entries" | "@@iterator" => {
8131            let items: Vec<Value> = with_host(|h| {
8132                let vals: Vec<Value> = match h.get(recv) {
8133                    Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
8134                    _ => Vec::new(),
8135                };
8136                if name == "entries" {
8137                    vals.into_iter()
8138                        .map(|v| h.new_array(vec![v.clone(), v]))
8139                        .collect()
8140                } else {
8141                    vals
8142                }
8143            });
8144            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8145        }
8146        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
8147    }
8148}
8149
8150fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8151    // An `async function*` object's methods return PROMISES of the record, and
8152    // its body has to be driven through the await-aware stepper (a plain
8153    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
8154    if host::is_async_generator(recv) {
8155        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
8156        // `.return`/`.throw` must wait behind a `.next()` that is still
8157        // suspended on an internal `await`, or that `.next()` would report
8158        // `{done: true}` for a value the body had not yet reached. An uncaught
8159        // `.throw(e)` rejects the returned promise; it does not throw here.
8160        return match name {
8161            "next" => Ok(host::async_gen_enqueue(
8162                recv,
8163                host::GenReq::Next(arg0(&args)),
8164            )),
8165            "return" => Ok(host::async_gen_enqueue(
8166                recv,
8167                host::GenReq::Return(arg0(&args)),
8168            )),
8169            "throw" => Ok(host::async_gen_enqueue(
8170                recv,
8171                host::GenReq::Throw(arg0(&args)),
8172            )),
8173            "@@asyncIterator" => Ok(recv.clone()),
8174            _ => Err(host::type_error(&format!(
8175                "asyncGenerator.{name} is not a function"
8176            ))),
8177        };
8178    }
8179    match name {
8180        "next" => {
8181            let send = arg0(&args);
8182            match host::gen_resume(recv, send)? {
8183                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8184                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8185            }
8186        }
8187        "return" => {
8188            // Resume with an injected return so any pending `finally` runs; the
8189            // completion may itself be a `finally` yield (not-done) or the value.
8190            match host::gen_return(recv, arg0(&args))? {
8191                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8192                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8193            }
8194        }
8195        "throw" => {
8196            // Inject a throw at the suspension point: an enclosing `try/catch` in
8197            // the body can handle it (and any `finally` runs); otherwise it
8198            // propagates to the caller.
8199            match host::gen_throw(recv, arg0(&args))? {
8200                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
8201                host::GenStep::Done(v) => Ok(iter_result(v, true)),
8202            }
8203        }
8204        _ => Err(host::type_error(&format!(
8205            "generator.{name} is not a function"
8206        ))),
8207    }
8208}
8209
8210/// A `{ value, done }` iterator-result object.
8211fn iter_result(value: Value, done: bool) -> Value {
8212    with_host(|h| {
8213        let mut m: IndexMap<String, Value> = IndexMap::new();
8214        m.insert("value".into(), value);
8215        m.insert("done".into(), Value::Bool(done));
8216        h.new_object(m)
8217    })
8218}
8219
8220/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
8221/// cursor over a materialized item list.
8222fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8223    match name {
8224        "next" => {
8225            let step = with_host(|h| {
8226                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
8227                    if *idx < items.len() {
8228                        let v = items[*idx].clone();
8229                        *idx += 1;
8230                        return Some(v);
8231                    }
8232                }
8233                None
8234            });
8235            Ok(match step {
8236                Some(v) => iter_result(v, false),
8237                None => iter_result(Value::Undef, true),
8238            })
8239        }
8240        "return" => {
8241            // Exhaust the cursor and report done.
8242            with_host(|h| {
8243                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
8244                    *idx = items.len();
8245                }
8246            });
8247            Ok(iter_result(arg0(&args), true))
8248        }
8249        // An iterator is its own iterable.
8250        "@@iterator" => Ok(recv.clone()),
8251        _ => Err(host::type_error(&format!(
8252            "iterator.{name} is not a function"
8253        ))),
8254    }
8255}
8256
8257fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
8258    match name {
8259        "toString" => Ok(with_host(|h| {
8260            let s = h.str_of(recv);
8261            h.new_str(s)
8262        })),
8263        _ => Err(host::type_error(&format!(
8264            "symbol.{name} is not a function"
8265        ))),
8266    }
8267}
8268
8269// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
8270
8271fn object_create(args: Vec<Value>) -> Result<Value, String> {
8272    let proto = arg0(&args);
8273    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
8274    // `undefined` is NOT accepted — measured on node v26.7.0,
8275    // `Object.create(undefined)` is
8276    // `TypeError: Object prototype may only be an Object or null: undefined`,
8277    // where node-js quietly built a normal object.
8278    reject_bad_prototype(&proto)?;
8279    let obj = with_host(|h| h.new_object(IndexMap::new()));
8280    // `set_proto` records a null proto as an explicit null-prototype object.
8281    with_host(|h| h.set_proto(&obj, proto));
8282    // Optional second arg: a property-descriptor map.
8283    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
8284        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
8285            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
8286            _ => Vec::new(),
8287        });
8288        for (k, d) in entries {
8289            apply_descriptor(&obj, &k, &d);
8290        }
8291    }
8292    Ok(obj)
8293}
8294
8295/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
8296/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
8297/// `EventEmitter.prototype` (the one express mixes onto its app function).
8298fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
8299    match ns {
8300        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
8301        _ => None,
8302    }
8303}
8304
8305/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
8306/// from its `ownKeys` trap (the symbol half of the same list the string keys are
8307/// filtered out of); every other receiver answers from its property map.
8308fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
8309    if let Some(keys) = crate::proxy::own_keys(v)? {
8310        return Ok(keys
8311            .iter()
8312            .filter(|k| host::is_symbol_key(k))
8313            .map(|k| crate::proxy::key_value(k))
8314            .collect());
8315    }
8316    Ok(with_host(|h| h.own_symbol_keys(v)))
8317}
8318
8319/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
8320pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
8321    object_define_property(vec![obj.clone(), key, desc])
8322}
8323
8324/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
8325pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
8326    object_get_own_descriptor(vec![obj.clone(), key])
8327}
8328
8329fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
8330    let obj = arg0(&args);
8331    // A Proxy defines through its `defineProperty` trap; the target it forwards
8332    // to is where the ordinary path below finally runs.
8333    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
8334        let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8335        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
8336        if !with_host(|h| is_object_like(h, &desc)) {
8337            return Err(host::type_error(&format!(
8338                "Property description must be an object: {}",
8339                with_host(|h| h.str_of(&desc))
8340            )));
8341        }
8342        crate::proxy::define_property(&obj, &key, &desc)?;
8343        return Ok(obj);
8344    }
8345    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
8346    // target and a non-object descriptor each throw before anything is written.
8347    if !with_host(|h| is_object_like(h, &obj)) {
8348        return Err(host::type_error(
8349            "Object.defineProperty called on non-object",
8350        ));
8351    }
8352    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
8353    if !with_host(|h| is_object_like(h, &desc)) {
8354        return Err(host::type_error(&format!(
8355            "Property description must be an object: {}",
8356            with_host(|h| h.str_of(&desc))
8357        )));
8358    }
8359    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8360    apply_descriptor(&obj, &key, &desc);
8361    Ok(obj)
8362}
8363
8364/// Whether `v` is an Object in the language sense — anything `typeof` calls
8365/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
8366/// distinguish "an object" from a primitive.
8367fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
8368    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
8369}
8370
8371/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
8372/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
8373/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
8374/// `assign` family: each returned an empty result for `null` where node v26.7.0
8375/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
8376/// is coercible and keeps working (`Object.keys(1)` is `[]`).
8377fn require_object_coercible(v: &Value) -> Result<(), String> {
8378    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
8379        return Err(host::type_error(
8380            "Cannot convert undefined or null to object",
8381        ));
8382    }
8383    Ok(())
8384}
8385
8386/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
8387/// nor `null`, with V8's wording. Measured on node v26.7.0:
8388/// `Object.create("s")` is
8389/// `TypeError: Object prototype may only be an Object or null: s`.
8390fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
8391    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
8392        return Ok(());
8393    }
8394    Err(host::type_error(&format!(
8395        "Object prototype may only be an Object or null: {}",
8396        with_host(|h| h.str_of(proto))
8397    )))
8398}
8399
8400/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
8401///
8402/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
8403/// `configurable` field defaults to **false** — which is why a `defineProperty`
8404/// data property is invisible to `Object.keys` unless the caller opts in. That
8405/// asymmetry against plain assignment is the whole reason the attribute table
8406/// exists.
8407fn apply_descriptor(obj: &Value, key: &str, desc: &Value) {
8408    let (value, get, set, attrs) = with_host(|h| match h.get(desc) {
8409        Some(JsObj::Object(p)) => {
8410            let flag = |n: &str| p.get(n).map(|v| h.truthy(v)).unwrap_or(false);
8411            (
8412                p.get("value").cloned(),
8413                p.get("get").cloned(),
8414                p.get("set").cloned(),
8415                host::PropAttrs {
8416                    writable: flag("writable"),
8417                    enumerable: flag("enumerable"),
8418                    configurable: flag("configurable"),
8419                },
8420            )
8421        }
8422        _ => (None, None, None, host::PropAttrs::default()),
8423    });
8424    with_host(|h| h.set_prop_attrs(obj, key, attrs));
8425    if get.is_some() || set.is_some() {
8426        with_host(|h| h.set_accessor(obj, key, get, set));
8427    } else if let Some(v) = value {
8428        // A function/class receiver stores its own props in the fn-prop side table
8429        // (express `mixin(app, proto)` defines methods onto the `app` *function*).
8430        if matches!(
8431            with_host(|h| h.get(obj).cloned()),
8432            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
8433        ) {
8434            with_host(|h| h.set_fn_prop(obj, key, v));
8435        } else if let (Some(ObjKind::Array), Ok(i)) =
8436            (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
8437        {
8438            // An array's index keys ARE its elements, and defining one past the
8439            // end grows the array with holes in between (10.4.2.1). This whole
8440            // branch used to be missing: `Object.defineProperty(arr, 1, {value})`
8441            // wrote into the ordinary property map an array does not have, so it
8442            // was a silent no-op.
8443            with_host(|h| {
8444                let old = match h.get(obj) {
8445                    Some(JsObj::Array(items)) => items.len(),
8446                    _ => 0,
8447                };
8448                if let Some(JsObj::Array(items)) = h.get_mut(obj) {
8449                    if i >= old {
8450                        items.resize(i + 1, Value::Undef);
8451                    }
8452                    items[i] = v;
8453                }
8454                if i > old {
8455                    h.mark_hole_range(obj, old..i);
8456                }
8457                h.clear_hole(obj, i);
8458            });
8459        } else {
8460            with_host(|h| {
8461                if let Some(JsObj::Object(p)) = h.get_mut(obj) {
8462                    p.insert(key.to_string(), v);
8463                    host::canonicalize_own_keys(p);
8464                }
8465            });
8466        }
8467    }
8468}
8469
8470/// `Object.defineProperties(obj, descriptorMap)`.
8471fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
8472    let obj = arg0(&args);
8473    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
8474    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
8475        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
8476        _ => Vec::new(),
8477    });
8478    for (k, d) in entries {
8479        apply_descriptor(&obj, &k, &d);
8480    }
8481    Ok(obj)
8482}
8483
8484fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
8485    let obj = arg0(&args);
8486    require_object_coercible(&obj)?;
8487    let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
8488    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
8489        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
8490    }
8491    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
8492    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
8493    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
8494        if let Some(names) = builtin_proto_method_names(&ns) {
8495            if names.contains(&key.as_str()) {
8496                return Ok(with_host(|h| {
8497                    let thunk = h.alloc(JsObj::Builtin(format!(
8498                        "@proto:{}:{key}",
8499                        ns.trim_end_matches(".prototype")
8500                    )));
8501                    let mut m: IndexMap<String, Value> = IndexMap::new();
8502                    m.insert("value".into(), thunk);
8503                    m.insert("writable".into(), Value::Bool(true));
8504                    m.insert("enumerable".into(), Value::Bool(true));
8505                    m.insert("configurable".into(), Value::Bool(true));
8506                    h.new_object(m)
8507                }));
8508            }
8509        }
8510    }
8511    // Accessor descriptor?
8512    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
8513        return Ok(with_host(|h| {
8514            let a = h.prop_attrs(&obj, &key);
8515            let mut m: IndexMap<String, Value> = IndexMap::new();
8516            m.insert("get".into(), get.unwrap_or(Value::Undef));
8517            m.insert("set".into(), set.unwrap_or(Value::Undef));
8518            m.insert("enumerable".into(), Value::Bool(a.enumerable));
8519            m.insert("configurable".into(), Value::Bool(a.configurable));
8520            h.new_object(m)
8521        }));
8522    }
8523    let val = with_host(|h| match h.get(&obj) {
8524        // A Buffer's own properties are exactly its byte indices, read out of the
8525        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
8526        // that V8 keeps on the prototype, so they own no descriptor.
8527        Some(JsObj::Object(p))
8528            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
8529        {
8530            match (
8531                p.get("@@bytes").and_then(|b| h.get(b)),
8532                key.parse::<usize>(),
8533            ) {
8534                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
8535                _ => None,
8536            }
8537        }
8538        Some(JsObj::Object(p)) => p.get(&key).cloned(),
8539        // An array's index keys read the elements; `length` is the exotic own
8540        // property; anything else is an ordinary own key in the side table.
8541        Some(JsObj::Array(items)) => match key.parse::<usize>() {
8542            // An ELIDED index owns no property at all, so it has no descriptor.
8543            Ok(i) if h.is_hole(&obj, i) => None,
8544            Ok(i) => items.get(i).cloned(),
8545            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
8546            Err(_) => h.fn_prop(&obj, &key),
8547        },
8548        // A function/class own prop lives in the fn-prop side table.
8549        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
8550        _ => None,
8551    });
8552    match val {
8553        Some(v) => Ok(with_host(|h| {
8554            let a = h.prop_attrs(&obj, &key);
8555            let mut m: IndexMap<String, Value> = IndexMap::new();
8556            m.insert("value".into(), v);
8557            m.insert("writable".into(), Value::Bool(a.writable));
8558            m.insert("enumerable".into(), Value::Bool(a.enumerable));
8559            m.insert("configurable".into(), Value::Bool(a.configurable));
8560            h.new_object(m)
8561        })),
8562        None => Ok(Value::Undef),
8563    }
8564}
8565
8566/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
8567/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
8568/// the standard "clone with accessors intact" idiom, so this must agree
8569/// key-for-key with `getOwnPropertyNames`.
8570fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
8571    let obj = arg0(&args);
8572    let names = object_keys(vec![obj.clone()], 3)?;
8573    let keys: Vec<String> = with_host(|h| match h.get(&names) {
8574        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
8575        _ => Vec::new(),
8576    });
8577    let mut out: IndexMap<String, Value> = IndexMap::new();
8578    for k in keys {
8579        let ks = with_host(|h| h.new_str(k.clone()));
8580        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
8581        if !matches!(d, Value::Undef) {
8582            out.insert(k, d);
8583        }
8584    }
8585    Ok(with_host(|h| h.new_object(out)))
8586}
8587
8588/// `key in obj` respecting the prototype chain. Reports a `Result` because a
8589/// Proxy's `has` trap is user code and may throw.
8590pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
8591    if let Some(b) = crate::proxy::has(obj, key)? {
8592        return Ok(b);
8593    }
8594    Ok(has_property_ordinary(obj, key))
8595}
8596
8597/// `[[HasProperty]]` for every non-Proxy receiver.
8598fn has_property_ordinary(obj: &Value, key: &str) -> bool {
8599    // `key in <builtin namespace/prototype>`: membership matches what a property
8600    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
8601    // prototype methods) resolve as callable thunks via `namespace_property`, so
8602    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
8603    // with the `in` operator before reading the intrinsic).
8604    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
8605        return !matches!(namespace_property(&ns, key), Value::Undef);
8606    }
8607    // An integer index of a typed array / Buffer is an own property, and lives
8608    // in the hidden element array rather than the property map — the same
8609    // question `hasOwnProperty` answers, through the same helper. Only a hit
8610    // short-circuits: a non-index key like `'length'` must still fall through
8611    // to the ordinary chain lookup below.
8612    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
8613        return true;
8614    }
8615    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
8616        return true;
8617    }
8618    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
8619        return true;
8620    }
8621    with_host(|h| match h.get(obj) {
8622        Some(JsObj::Object(p)) => p.contains_key(key),
8623        Some(JsObj::Array(items)) => {
8624            key == "length"
8625                || key
8626                    .parse::<usize>()
8627                    .map(|i| i < items.len() && !h.is_hole(obj, i))
8628                    .unwrap_or(false)
8629                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
8630                // side table, and `in` must see it.
8631                || h.fn_prop(obj, key).is_some()
8632        }
8633        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
8634        _ => false,
8635    })
8636}
8637
8638/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
8639/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
8640/// that preserves the *reference graph*. Two properties pointing at the same
8641/// object clone to two properties pointing at the same clone, and a cycle clones
8642/// to a cycle instead of recursing forever. `seen` maps each source heap index
8643/// to its clone, which is what buys both.
8644pub(crate) fn deep_clone(v: &Value) -> Value {
8645    deep_clone_seen(v, &mut std::collections::HashMap::new())
8646}
8647
8648fn deep_clone_seen(v: &Value, seen: &mut std::collections::HashMap<u32, Value>) -> Value {
8649    let idx = match v {
8650        Value::Obj(i) => *i,
8651        _ => return v.clone(),
8652    };
8653    if let Some(done) = seen.get(&idx) {
8654        return done.clone();
8655    }
8656    match with_host(|h| h.get(v).cloned()) {
8657        Some(JsObj::Array(items)) => {
8658            // Register the (empty) clone BEFORE recursing so a self-reference
8659            // resolves to it.
8660            let out = with_host(|h| h.new_array(Vec::new()));
8661            seen.insert(idx, out.clone());
8662            let cloned: Vec<Value> = items.iter().map(|x| deep_clone_seen(x, seen)).collect();
8663            with_host(|h| {
8664                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
8665                    *a = cloned;
8666                }
8667                // A sparse source clones to an equally sparse array: the clone
8668                // walks own properties, so a hole is nothing to copy.
8669                h.copy_holes(v, &out, Some);
8670            });
8671            out
8672        }
8673        Some(JsObj::Object(props)) => {
8674            let out = with_host(|h| h.new_object(IndexMap::new()));
8675            seen.insert(idx, out.clone());
8676            let cloned: IndexMap<String, Value> = props
8677                .iter()
8678                .map(|(k, val)| (k.clone(), deep_clone_seen(val, seen)))
8679                .collect();
8680            with_host(|h| {
8681                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
8682                    *p = cloned;
8683                }
8684                // A native exotic (Buffer, typed array, …) keeps its prototype so
8685                // the clone passes the same brand checks as the source.
8686                if let Some(p) = h.proto_of(v) {
8687                    h.set_proto(&out, p);
8688                }
8689                h.copy_prop_attrs(v, &out);
8690            });
8691            out
8692        }
8693        // Map/Set are structured types: clone the entries, keep the kind.
8694        Some(JsObj::Map { entries, weak }) => {
8695            let out = with_host(|h| {
8696                h.alloc(JsObj::Map {
8697                    entries: IndexMap::new(),
8698                    weak,
8699                })
8700            });
8701            seen.insert(idx, out.clone());
8702            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
8703            for (k, val) in pairs {
8704                let ck = deep_clone_seen(&k, seen);
8705                let cv = deep_clone_seen(&val, seen);
8706                let _ = map_method(&out, "set", vec![ck, cv]);
8707            }
8708            out
8709        }
8710        Some(JsObj::Set { entries, weak }) => {
8711            let out = with_host(|h| {
8712                h.alloc(JsObj::Set {
8713                    entries: IndexMap::new(),
8714                    weak,
8715                })
8716            });
8717            seen.insert(idx, out.clone());
8718            let vals: Vec<Value> = entries.values().cloned().collect();
8719            for x in vals {
8720                let cx = deep_clone_seen(&x, seen);
8721                let _ = set_method(&out, "add", vec![cx]);
8722            }
8723            out
8724        }
8725        // Strings/BigInts/RegExps/dates are immutable-enough to share, and a
8726        // function is not cloneable at all (Node throws DataCloneError; node-js
8727        // passes it through rather than inventing that error class).
8728        _ => v.clone(),
8729    }
8730}
8731
8732// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
8733
8734/// A short `Name: message` string for an error value (used when an await
8735/// rejection unwinds as a thrown error).
8736pub fn error_string(h: &host::JsHost, v: &Value) -> String {
8737    if let Some(JsObj::Object(props)) = h.get(v) {
8738        let name = props
8739            .get("name")
8740            .map(|x| h.str_of(x))
8741            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
8742            .unwrap_or_else(|| "Error".into());
8743        if let Some(m) = props.get("message") {
8744            return format!("{name}: {}", h.str_of(m));
8745        }
8746        return name;
8747    }
8748    h.str_of(v)
8749}
8750
8751fn make_builtin(name: String) -> Value {
8752    with_host(|h| h.alloc(JsObj::Builtin(name)))
8753}
8754
8755/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
8756/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
8757///
8758/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
8759/// an EXPLICIT link, so an object on the default prototype reported `null`:
8760/// `({}).__proto__ === Object.prototype` was false while
8761/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
8762/// the three cannot drift apart again.
8763pub fn prototype_of(v: &Value) -> Value {
8764    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
8765    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
8766    // not `Function.prototype`. This is the class-side half of the subclass
8767    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
8768    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
8769        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
8770    }
8771    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
8772    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
8773    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
8774    // `ClassVal.parent`, but the link itself was invisible, so
8775    // `Object.getPrototypeOf(B) === A` read false and any library walking the
8776    // constructor chain — rather than calling a static — saw a base class.
8777    // A base class keeps the default answer below (`Function.prototype`).
8778    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
8779        if let Some(parent) = c.parent {
8780            return parent;
8781        }
8782    }
8783    // `Object.create(null)` and friends really do have a null prototype.
8784    if with_host(|h| h.has_null_proto(v)) {
8785        return with_host(|h| h.null());
8786    }
8787    if let Some(p) = with_host(|h| h.proto_of(v)) {
8788        return p;
8789    }
8790    // A builtin exotic with no explicit `[[Prototype]]` link reports its
8791    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
8792    // Array.prototype`), which `strict_eq` compares by name. A plain object
8793    // reports the one real `Object.prototype` object.
8794    with_host(|h| {
8795        h.ensure_native_protos();
8796        match default_ctor_name(h, v) {
8797            Some("Object") => h.object_proto(),
8798            Some(c) => h.alloc(JsObj::Builtin(format!("{c}.prototype"))),
8799            None => h.null(),
8800        }
8801    })
8802}
8803
8804/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
8805/// internal resolve/reject functions.
8806fn new_promise(executor: Value) -> Result<Value, String> {
8807    let p = with_host(|h| h.new_promise());
8808    let id = with_host(|h| h.promise_id(&p).unwrap());
8809    let res = make_builtin(format!("@@presolve:{id}"));
8810    let rej = make_builtin(format!("@@preject:{id}"));
8811    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
8812        // A throw in the executor rejects the promise.
8813        let ev = host::take_exc_or_error(&e);
8814        host::reject_promise_val(id, ev);
8815    }
8816    Ok(p)
8817}
8818
8819fn promise_resolve(v: Value) -> Result<Value, String> {
8820    Ok(host::promise_of(&v))
8821}
8822fn promise_reject(v: Value) -> Result<Value, String> {
8823    let p = with_host(|h| h.new_promise());
8824    let id = with_host(|h| h.promise_id(&p).unwrap());
8825    host::reject_promise_val(id, v);
8826    Ok(p)
8827}
8828
8829/// `Promise.withResolvers()` — a fresh pending promise paired with its own
8830/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
8831/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
8832fn promise_with_resolvers() -> Result<Value, String> {
8833    let p = with_host(|h| h.new_promise());
8834    let id = with_host(|h| h.promise_id(&p).unwrap());
8835    let resolve = make_builtin(format!("@@presolve:{id}"));
8836    let reject = make_builtin(format!("@@preject:{id}"));
8837    let mut props: IndexMap<String, Value> = IndexMap::new();
8838    props.insert("promise".into(), p);
8839    props.insert("resolve".into(), resolve);
8840    props.insert("reject".into(), reject);
8841    Ok(with_host(|h| h.new_object(props)))
8842}
8843
8844#[derive(Clone, Copy)]
8845enum AllMode {
8846    All,
8847    AllSettled,
8848}
8849
8850/// `Promise.all` / `Promise.allSettled`.
8851fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
8852    let items = host::iter_all(&arg0(&args))?;
8853    let result = with_host(|h| h.new_promise());
8854    let rid = with_host(|h| h.promise_id(&result).unwrap());
8855    let n = items.len();
8856    if n == 0 {
8857        let empty = with_host(|h| h.new_array(Vec::new()));
8858        host::resolve_promise_val(rid, empty);
8859        return Ok(result);
8860    }
8861    // Shared mutable accumulator via Rc<RefCell<…>>.
8862    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
8863    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
8864    for (i, it) in items.into_iter().enumerate() {
8865        let ap = host::promise_of(&it);
8866        let aid = with_host(|h| h.promise_id(&ap).unwrap());
8867        let slots = slots.clone();
8868        let remaining = remaining.clone();
8869        host::subscribe_native(
8870            aid,
8871            Box::new(move |state, val| {
8872                let settled = match mode {
8873                    AllMode::All => {
8874                        if state == host::PromiseState::Rejected {
8875                            host::reject_promise_val(rid, val);
8876                            return Ok(());
8877                        }
8878                        val
8879                    }
8880                    AllMode::AllSettled => with_host(|h| {
8881                        let mut m: IndexMap<String, Value> = IndexMap::new();
8882                        if state == host::PromiseState::Rejected {
8883                            m.insert("status".into(), h.new_str("rejected"));
8884                            m.insert("reason".into(), val);
8885                        } else {
8886                            m.insert("status".into(), h.new_str("fulfilled"));
8887                            m.insert("value".into(), val);
8888                        }
8889                        h.new_object(m)
8890                    }),
8891                };
8892                slots.borrow_mut()[i] = settled;
8893                let mut r = remaining.borrow_mut();
8894                *r -= 1;
8895                if *r == 0 {
8896                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
8897                    host::resolve_promise_val(rid, arr);
8898                }
8899                Ok(())
8900            }),
8901        );
8902    }
8903    Ok(result)
8904}
8905
8906/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
8907fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
8908    let items = host::iter_all(&arg0(&args))?;
8909    let result = with_host(|h| h.new_promise());
8910    let rid = with_host(|h| h.promise_id(&result).unwrap());
8911    let n = items.len();
8912    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
8913    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
8914    for (i, it) in items.into_iter().enumerate() {
8915        let ap = host::promise_of(&it);
8916        let aid = with_host(|h| h.promise_id(&ap).unwrap());
8917        let errors = errors.clone();
8918        let remaining = remaining.clone();
8919        host::subscribe_native(
8920            aid,
8921            Box::new(move |state, val| {
8922                if any {
8923                    if state == host::PromiseState::Fulfilled {
8924                        host::resolve_promise_val(rid, val);
8925                    } else {
8926                        errors.borrow_mut()[i] = val;
8927                        let mut r = remaining.borrow_mut();
8928                        *r -= 1;
8929                        if *r == 0 {
8930                            // All rejected → AggregateError carrying every reason.
8931                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
8932                            let msg = with_host(|h| h.new_str("All promises were rejected"));
8933                            let agg = make_error("AggregateError", &[reasons, msg]);
8934                            host::reject_promise_val(rid, agg);
8935                        }
8936                    }
8937                } else if state == host::PromiseState::Rejected {
8938                    host::reject_promise_val(rid, val);
8939                } else {
8940                    host::resolve_promise_val(rid, val);
8941                }
8942                Ok(())
8943            }),
8944        );
8945    }
8946    Ok(result)
8947}
8948
8949/// `.then` / `.catch` / `.finally` on a promise.
8950fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
8951    match name {
8952        "then" => Ok(host::promise_then(
8953            recv,
8954            args.first().cloned().unwrap_or(Value::Undef),
8955            args.get(1).cloned().unwrap_or(Value::Undef),
8956        )),
8957        "catch" => Ok(host::promise_then(
8958            recv,
8959            Value::Undef,
8960            args.first().cloned().unwrap_or(Value::Undef),
8961        )),
8962        "finally" => {
8963            let cb = arg0(&args);
8964            let i = match cb {
8965                Value::Obj(i) => i,
8966                _ => 0,
8967            };
8968            let pass = make_builtin(format!("@@finpass:{i}"));
8969            let throw = make_builtin(format!("@@finthrow:{i}"));
8970            Ok(host::promise_then(recv, pass, throw))
8971        }
8972        _ => Err(host::type_error(&format!(
8973            "promise.{name} is not a function"
8974        ))),
8975    }
8976}
8977
8978fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
8979    with_host(|h| {
8980        if next_tick {
8981            h.queue_nexttick(cb, args);
8982        } else {
8983            h.queue_micro(cb, args);
8984        }
8985    });
8986}
8987
8988/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
8989/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
8990/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
8991///
8992/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
8993/// fires, so it runs until cleared and — being referenced — holds the process
8994/// open exactly as in Node.
8995fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
8996    let cb = arg0(&args);
8997    let delay = if name == "setImmediate" {
8998        -1.0 // before any 0ms timeout
8999    } else {
9000        args.get(1)
9001            .map(|d| with_host(|h| h.to_number(d)))
9002            .unwrap_or(0.0)
9003            .max(0.0)
9004    };
9005    let extra = if name == "setImmediate" {
9006        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
9007    } else {
9008        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
9009    };
9010    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
9011    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
9012    let interval = (name == "setInterval").then(|| delay.max(1.0));
9013    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
9014    let tag = if name == "setImmediate" {
9015        "Immediate"
9016    } else {
9017        "Timeout"
9018    };
9019    crate::stdlib::timers::new_handle(id, tag)
9020}
9021
9022/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
9023/// by the bare id it coerces to (code that stored `+timer` still works).
9024fn clear_timer(v: &Value) {
9025    let id =
9026        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
9027    with_host(|h| h.cancel_timer(id));
9028}