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::FORIN_ALIVE, b_forin_alive);
38    vm.register_builtin(ops::HOIST_TDZ, b_hoist_tdz);
39    vm.register_builtin(ops::NEW_SPREAD, b_new_spread);
40    vm.register_builtin(ops::SUPER_CALL_SPREAD, b_super_call_spread);
41    vm.register_builtin(ops::CONTAINS, b_contains);
42    vm.register_builtin(ops::SIG_RETURN, b_sig_return);
43    vm.register_builtin(ops::BINOP, b_binop);
44    vm.register_builtin(ops::UNARY, b_unary);
45    vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
46    vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
47    vm.register_builtin(ops::TYPEOF, b_typeof);
48    vm.register_builtin(ops::LOAD_NULL, b_load_null);
49    vm.register_builtin(ops::THROW, b_throw);
50    vm.register_builtin(ops::TRY, b_try);
51    vm.register_builtin(ops::NULLISH, b_nullish);
52    vm.register_builtin(ops::UNPACK, b_unpack);
53    vm.register_builtin(ops::BUILD_ARGS, b_build_args);
54    vm.register_builtin(ops::THIS, b_this);
55    vm.register_builtin(ops::INSTANCEOF, b_instanceof);
56    vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
57    vm.register_builtin(ops::APPLY, b_apply);
58    vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
59    vm.register_builtin(ops::OBJ_REST, b_obj_rest);
60    vm.register_builtin(ops::DIV, b_div);
61    vm.register_builtin(ops::POW, b_pow);
62    vm.register_builtin(ops::MKCLASS, b_mkclass);
63    vm.register_builtin(ops::DEF_MEMBER, b_def_member);
64    vm.register_builtin(ops::DEF_FIELD, b_def_field);
65    vm.register_builtin(ops::SUPER_CALL, b_super_call);
66    vm.register_builtin(ops::SUPER_GET, b_super_get);
67    vm.register_builtin(ops::YIELD, b_yield);
68    vm.register_builtin(ops::PROPKEY, b_propkey);
69    vm.register_builtin(ops::NEW_TARGET, b_new_target);
70    vm.register_builtin(ops::AWAIT, b_await);
71    vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
72    vm.register_builtin(ops::DBG_LINE, b_dbg_line);
73    vm.register_builtin(ops::MKBIGINT, b_mkbigint);
74    vm.register_builtin(ops::MKREGEX, b_mkregex);
75    vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
76    vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
77    vm.register_builtin(ops::ASYNC_STEP, b_async_step);
78    vm.register_builtin(ops::NUM_STEP, b_num_step);
79    vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
80    vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
81    vm.register_builtin(ops::SIG_BREAK, b_sig_break);
82    vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
83    vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
84    vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
85    vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
86    vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
87    vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
88    vm.register_builtin(ops::HOIST_VAR, b_hoist_var);
89    vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
90}
91
92/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
93/// runs its pending `finally`; a user iterator object gets its `.return()` called
94/// if present; a plain materialized iterator just drops. Returns `undefined`.
95/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
96/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
97/// is neither is left alone.
98pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
99    if with_host(|h| h.is_generator_val(it)) {
100        host::gen_return(it, Value::Undef)?;
101        return Ok(());
102    }
103    if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
104        if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
105            if with_host(|h| host::is_callable(h, &f)) {
106                host::invoke(&f, Vec::new(), Some(it.clone()))?;
107            }
108        }
109    }
110    Ok(())
111}
112
113fn b_iter_close(vm: &mut VM, _: u8) -> Value {
114    let it = vm.pop();
115    // A `finally` may print or yield, but the loop is done either way; an error
116    // it raises still propagates.
117    match close_iterator(&it) {
118        Ok(()) => Value::Undef,
119        Err(e) => abort(vm, e),
120    }
121}
122
123/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
124/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
125/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
126/// Number nor throws the mix error.
127fn b_num_step(vm: &mut VM, _: u8) -> Value {
128    let old = vm.pop();
129    let tag = match vm.pop() {
130        Value::Int(n) => n,
131        Value::Float(f) => f as i64,
132        _ => 1,
133    };
134    if with_host(|h| h.is_bigint_val(&old)) {
135        let b = with_host(|h| h.as_bigint(&old)).unwrap();
136        let old_n = with_host(|h| h.new_bigint(b.clone()));
137        let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
138        vm.push(old_n);
139        new
140    } else {
141        let n = with_host(|h| h.to_number(&old));
142        vm.push(Value::Float(n));
143        Value::Float(n + tag as f64)
144    }
145}
146
147/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
148/// `{value, done}` record (see `host::async_step`).
149fn b_async_step(vm: &mut VM, _: u8) -> Value {
150    let iter = vm.pop();
151    let r = host::async_step(&iter);
152    finish(vm, r)
153}
154
155/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
156/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
157fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
158    let digits = sval(&vm.pop());
159    match digits.parse::<num_bigint::BigInt>() {
160        Ok(b) => with_host(|h| h.new_bigint(b)),
161        Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
162    }
163}
164
165/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
166/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
167/// Builds the `strings` array (carrying its `.raw` array) and calls
168/// `tag(strings, ...values)`.
169/// Reject a non-callable where node's scheduling entry points demand one.
170///
171/// Every one of them validates SYNCHRONOUSLY — `try { queueMicrotask(1) }
172/// catch` catches an `ERR_INVALID_ARG_TYPE` in node. Here the value was queued
173/// unchecked and the failure surfaced from the event loop instead, as an
174/// uncaught `1 is not a function` that killed the process past any `try` around
175/// the call.
176fn require_callback(cb: &Value) -> Result<(), String> {
177    if with_host(|h| host::is_callable(h, cb)) {
178        return Ok(());
179    }
180    Err(host::invalid_arg_type(
181        "callback", "argument", "function", cb,
182    ))
183}
184
185fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
186    // The chunk holding this site, read before the operands are popped and
187    // before any host borrow: together with the compiler's per-site ordinal it
188    // names the Parse Node whose template object 13.2.8.4 caches.
189    let chunk = vm.chunk.op_hash;
190    let mut all = pop_n(vm, argc as usize);
191    let int_of = |v: &Value| match v {
192        Value::Int(n) => *n as usize,
193        Value::Float(f) => *f as usize,
194        _ => 0,
195    };
196    let this = all.remove(0);
197    let tag = all.remove(0);
198    let n = int_of(&all.remove(0));
199    let mcount = int_of(&all.remove(0));
200    let site = int_of(&all.remove(0)) as u64;
201    let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
202    let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
203    let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
204    // GetTemplateObject caches by Parse Node, so a site evaluated twice hands
205    // back the SAME object — the whole point of the caching, since a tag that
206    // memoizes on the strings array (lit-html, graphql-tag) re-parses its
207    // template on every call without it.
208    let key = (chunk, site);
209    let strings = match with_host(|h| h.template_object(key)) {
210        Some(cached) => cached,
211        None => {
212            // strings = cooked array; strings.raw = raw array.
213            let strings = with_host(|h| h.new_array(cooked));
214            let raw_arr = with_host(|h| h.new_array(raw));
215            // `raw` is an own property that is neither writable, enumerable, nor
216            // configurable, so it stays out of `Object.keys(strings)` while
217            // `getOwnPropertyNames` still reports it.
218            with_host(|h| {
219                h.set_fn_prop(&strings, "raw", raw_arr.clone());
220                h.set_prop_attrs(
221                    &strings,
222                    "raw",
223                    host::PropAttrs {
224                        writable: false,
225                        enumerable: false,
226                        configurable: false,
227                    },
228                );
229                // Steps 12-13 run SetIntegrityLevel(frozen) on the raw array and
230                // then on the template object itself. Without them a tag could
231                // write through its own strings array and corrupt every later
232                // evaluation of the site — which is exactly what caching makes
233                // reachable, so the freeze and the cache belong together.
234                h.seal_object(&raw_arr, true);
235                h.seal_object(&strings, true);
236                h.set_template_object(key, strings.clone());
237            });
238            strings
239        }
240    };
241    let mut call_args = vec![strings];
242    call_args.extend(values);
243    let this = match this {
244        Value::Undef => None,
245        v => Some(v),
246    };
247    let r = host::invoke(&tag, call_args, this);
248    finish(vm, r)
249}
250
251/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
252/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
253/// iterator (each yielded value is awaited). Returns the iterator object/handle.
254fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
255    let src = vm.pop();
256    let r = host::get_async_iterator(&src).map_err(|e| {
257        // `for await` names the source AND says ASYNC: `for await (const x of
258        // o)` is `o is not async iterable`. Built here rather than through
259        // `name_call_site`, whose suffix table has no entry that composes.
260        match host::call_site_text(vm) {
261            Some(t) if e.ends_with(" is not iterable") => {
262                host::type_error(&format!("{t} is not async iterable"))
263            }
264            _ => e,
265        }
266    });
267    finish(vm, r)
268}
269
270/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
271/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
272/// express (backreference/lookaround) throws a `SyntaxError` here.
273fn b_mkregex(vm: &mut VM, _: u8) -> Value {
274    let flags = sval(&vm.pop());
275    let pattern = sval(&vm.pop());
276    match crate::regexp::build_regexp(&pattern, &flags) {
277        Ok(v) => v,
278        Err(e) => abort(vm, e),
279    }
280}
281
282/// DAP per-statement marker (`node --dap` only; the compiler emits this before
283/// each statement under `debug`). Pops the source line pushed by the preceding
284/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
285/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
286/// session is active.
287fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
288    let line = match vm.pop() {
289        Value::Int(n) => n as u32,
290        _ => 0,
291    };
292    crate::dap::on_debug_line(line);
293    Value::Undef
294}
295
296/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
297/// or `member::SET`). Keeps the object on the stack.
298fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
299    let func = vm.pop();
300    let kind = match vm.pop() {
301        Value::Int(n) => n,
302        _ => 0,
303    };
304    let name = sval(&vm.pop());
305    let obj = vm.pop();
306    with_host(|h| {
307        if kind == host::member::SET {
308            h.set_accessor(&obj, &name, None, Some(func));
309        } else {
310            h.set_accessor(&obj, &name, Some(func), None);
311        }
312    });
313    obj
314}
315
316fn b_await(vm: &mut VM, _: u8) -> Value {
317    let v = vm.pop();
318    match host::await_value(v) {
319        Ok(r) => r,
320        Err(e) => abort(vm, e),
321    }
322}
323
324// ── classes / super / generators / property keys (compiler-emitted ops) ──────
325
326fn b_mkclass(vm: &mut VM, _: u8) -> Value {
327    let ctor = vm.pop();
328    let parent = vm.pop();
329    let name = sval(&vm.pop());
330    host::build_class(&name, parent, ctor)
331}
332
333fn b_def_member(vm: &mut VM, _: u8) -> Value {
334    let func = vm.pop();
335    let is_static = matches!(vm.pop(), Value::Bool(true));
336    let kind = match vm.pop() {
337        Value::Int(n) => n,
338        _ => 0,
339    };
340    let name = sval(&vm.pop());
341    let class_val = vm.pop();
342    host::define_member(&class_val, &name, kind, is_static, func);
343    class_val
344}
345
346fn b_def_field(vm: &mut VM, _: u8) -> Value {
347    // `name_anon`: the initializer was an anonymous function definition, so
348    // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
349    // decided by the compiler, not re-derived from the produced value.
350    let name_anon = matches!(vm.pop(), Value::Bool(true));
351    let thunk = vm.pop();
352    let name = sval(&vm.pop());
353    let class_val = vm.pop();
354    host::define_field(&class_val, &name, thunk, name_anon);
355    class_val
356}
357
358/// `super(...args)` in a derived constructor: run the parent constructor on the
359/// current `this`, then this class's field initializers.
360/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
361/// run time. Shares everything below with the fixed-arity form; only where the
362/// arguments come from differs.
363fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
364    let arr = vm.pop();
365    let args = host::iter_all(&arr).unwrap_or_default();
366    super_call_with(vm, args)
367}
368
369fn b_super_call(vm: &mut VM, argc: u8) -> Value {
370    let args = pop_n(vm, argc as usize);
371    super_call_with(vm, args)
372}
373
374fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
375    let this = with_host(|h| h.current_this());
376    let this = match this {
377        Some(t) => t,
378        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
379    };
380    // The class whose constructor is running = the running method's home class.
381    let (parent, fields) = with_host(|h| h.super_context());
382    let (parent, fields) = match parent {
383        Some(p) => (p, fields),
384        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
385    };
386    let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
387    let this = match host::super_construct(&parent, args, &this, &nt) {
388        Err(e) => return abort(vm, e),
389        // The parent returned an object of its own: 15.7.15 makes THAT the
390        // instance, so `this` is rebound to it for the rest of the constructor
391        // and it is what `new` hands back.
392        Ok(Some(replacement)) => {
393            with_host(|h| h.set_current_this(replacement.clone()));
394            replacement
395        }
396        Ok(None) => this,
397    };
398    if !with_host(|h| h.bind_super_this()) {
399        return abort(
400            vm,
401            "ReferenceError: Super constructor may only be called once".to_string(),
402        );
403    }
404    // Run this (derived) class's own instance-field initializers after super.
405    for (name, thunk, name_anon) in fields {
406        if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
407            return abort(vm, e);
408        }
409    }
410    Value::Undef
411}
412
413/// `super.name` — a method from the parent's prototype, or a getter's result.
414fn b_super_get(vm: &mut VM, _: u8) -> Value {
415    let name = sval(&vm.pop());
416    match with_host(|h| h.super_resolve(&name)) {
417        host::SuperRef::Data(v) => v,
418        host::SuperRef::Getter(getter) => {
419            let this = with_host(|h| h.current_this());
420            match host::invoke(&getter, Vec::new(), this) {
421                Ok(v) => v,
422                Err(e) => abort(vm, e),
423            }
424        }
425    }
426}
427
428/// Close every loop iterator parked on `vm`'s stack at the op now executing,
429/// innermost first. Called where a chunk is about to be halted abruptly, since
430/// the code that would ordinarily close them is being jumped over.
431///
432/// A close runs user code (a generator's `finally`), which can itself throw; the
433/// error is deliberately dropped, because it must not replace the completion
434/// that caused the unwind.
435fn close_parked_iters(vm: &mut VM) {
436    let n = host::parked_iters(vm);
437    if n == 0 {
438        return;
439    }
440    // The completion that caused the unwind is already pending on the host.
441    // Closing an iterator resumes ANOTHER generator, which settles its own
442    // signal/error state, so the pending one is saved across the close and put
443    // back — otherwise the outer `.return()` would be lost.
444    let saved = with_host(|h| (h.signal.take(), h.error.take()));
445    for _ in 0..n {
446        let it = vm.pop();
447        let _ = close_iterator(&it);
448    }
449    with_host(|h| {
450        h.signal = saved.0;
451        h.error = saved.1;
452    });
453}
454
455fn b_yield(vm: &mut VM, _: u8) -> Value {
456    let v = vm.pop();
457    match host::gen_yield(v) {
458        Ok(sent) => {
459            // A `.return()`/`.throw()` injected on resume sets a pending Return
460            // signal (or error); halt the chunk so the body unwinds through any
461            // enclosing `try/finally`, exactly like a source `return`/`throw`.
462            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
463                // Halting jumps past the loop exits, so the `for…of` / `yield*`
464                // iterators parked on this chunk's stack would be abandoned
465                // still-suspended. They sit directly beneath the yielded value
466                // (innermost last), and the compiler recorded how many are
467                // there for this exact op.
468                close_parked_iters(vm);
469                vm.ip = vm.chunk.ops.len();
470            }
471            sent
472        }
473        // An injected `.throw()` comes back as an error rather than a signal,
474        // and abandons the parked iterators the same way. The thrown value is
475        // already on the host as `exc`; `close_parked_iters` puts back whatever
476        // it saves, so the close cannot swallow it.
477        Err(e) => {
478            close_parked_iters(vm);
479            abort(vm, e)
480        }
481    }
482}
483
484/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
485///
486/// It called `JsHost::property_key` directly, which is the primitive-only half
487/// of the conversion, so an object key never ran `ToPrimitive`:
488/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
489/// member form `a[o] = 1` — which does go through `host::to_property_key` —
490/// keyed on `"TS"`. The two forms are the same abstract operation and now share
491/// the same implementation.
492fn b_propkey(vm: &mut VM, _: u8) -> Value {
493    let v = vm.pop();
494    match host::to_property_key(&v) {
495        Ok(k) => with_host(|h| h.new_str(k)),
496        Err(e) => abort(vm, e),
497    }
498}
499
500fn b_new_target(_vm: &mut VM, _: u8) -> Value {
501    with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
502}
503
504/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
505/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
506/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
507/// `0/0 === NaN`, so `/` is lowered here instead.
508///
509/// Being a builtin rather than a native op means it does NOT reach the numeric
510/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
511/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
512/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
513/// hook now, so `/` coerces exactly as `*` and `-` do.
514fn b_div(vm: &mut VM, _: u8) -> Value {
515    let b = vm.pop();
516    let a = vm.pop();
517    let r = numeric_hook(NumOp::Div, &a, &b);
518    finish(vm, r)
519}
520
521/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
522/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
523/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
524/// one code path that already handles it.
525fn b_pow(vm: &mut VM, _: u8) -> Value {
526    let b = vm.pop();
527    let a = vm.pop();
528    let r = numeric_hook(NumOp::Pow, &a, &b);
529    finish(vm, r)
530}
531
532/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
533fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
534    let excluded = vm.pop();
535    let obj = vm.pop();
536    // The excluded keys are normalized exactly as a property READ normalizes
537    // them, not merely stringified: a symbol key lives on the object under its
538    // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
539    // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
540    // symbol-keyed property in `rest`.
541    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
542        .unwrap_or_default()
543        .iter()
544        .filter_map(|v| host::to_property_key(v).ok())
545        .collect();
546    // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
547    // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
548    // uses, so an ACCESSOR is in the list — reading the property map directly
549    // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
550    // object with no `g` and never ran the getter.
551    // A PROXY answers from its traps — `ownKeys`, then a
552    // `getOwnPropertyDescriptor` per key to test enumerability — which
553    // `own_enum_key_names` cannot see. Rest over one produced an empty object
554    // and ran no traps at all.
555    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
556        let keys = match crate::proxy::own_keys(&obj) {
557            Ok(k) => k.unwrap_or_default(),
558            Err(e) => return abort(vm, e),
559        };
560        let mut pairs: Vec<(String, Value)> = Vec::new();
561        for k in keys {
562            if excl.contains(&k) {
563                continue;
564            }
565            // The enumerability test and the READ interleave per key, as node's
566            // trap log shows — testing every key first and then reading them
567            // all produced the right object through the wrong trap sequence.
568            match crate::proxy::own_enumerable(&obj, &k) {
569                Ok(false) => continue,
570                Ok(true) => {}
571                Err(e) => return abort(vm, e),
572            }
573            match get_property(&obj, &k) {
574                Ok(v) => pairs.push((k, v)),
575                Err(e) => return abort(vm, e),
576            }
577        }
578        return with_host(|h| h.new_object(pairs.into_iter().collect()));
579    }
580    let keys: Vec<String> = with_host(|h| {
581        // `own_enum_key_names` is the STRING half — the same list `Object.keys`
582        // gives, so an accessor is in it. The symbol-keyed half lives in the
583        // property map under the internal `@@sym:` spelling and has to be
584        // collected separately, since `Object.keys` deliberately omits it.
585        let mut ks = h.own_enum_key_names(&obj);
586        if let Some(JsObj::Object(m)) = h.get(&obj) {
587            for k in m.keys() {
588                if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
589                    ks.push(k.clone());
590                }
591            }
592        }
593        ks
594    })
595    .into_iter()
596    .filter(|k| {
597        // An internal slot (`@@native`, `@@bytes`, …) or a private class field
598        // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
599        // the two cases cannot be told apart by the prefix alone.
600        !excl.contains(k)
601            && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
602    })
603    .collect();
604    // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
605    // user code and re-entering the VM under the borrow aborts the process.
606    let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
607    for k in keys {
608        match get_property(&obj, &k) {
609            Ok(v) => pairs.push((k, v)),
610            Err(e) => return abort(vm, e),
611        }
612    }
613    with_host(|h| {
614        let props: IndexMap<String, Value> = pairs.into_iter().collect();
615        h.new_object(props)
616    })
617}
618
619// ── helpers ──────────────────────────────────────────────────────────────────
620
621fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
622    let mut v = Vec::with_capacity(n);
623    for _ in 0..n {
624        v.push(vm.pop());
625    }
626    v.reverse();
627    v
628}
629
630/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
631fn sval(v: &Value) -> String {
632    if let Value::Str(s) = v {
633        return (**s).clone();
634    }
635    with_host(|h| h.as_str(v)).unwrap_or_default()
636}
637
638/// The same string, without `sval`'s deep copy. Every identifier the compiler
639/// emits is a `Value::Str` constant, so a variable read or write that went
640/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
641/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
642/// handle is a refcount bump instead.
643fn sname(v: &Value) -> std::sync::Arc<String> {
644    match v {
645        Value::Str(s) => s.clone(),
646        _ => std::sync::Arc::new(sval(v)),
647    }
648}
649
650fn abort(vm: &mut VM, e: String) -> Value {
651    with_host(|h| h.error = Some(e));
652    vm.ip = vm.chunk.ops.len();
653    Value::Undef
654}
655
656/// Halt the chunk if a call left an error or non-local signal pending.
657fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
658    match r {
659        Ok(v) => {
660            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
661                vm.ip = vm.chunk.ops.len();
662            }
663            v
664        }
665        Err(e) => abort(vm, e),
666    }
667}
668
669// ── name handlers ─────────────────────────────────────────────────────────────
670
671/// The value a bare global identifier resolves to, or `None` if unbound.
672///
673/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
674/// which must agree: a name reachable one way and not the other is exactly the
675/// discrepancy that left `globalThis.process` undefined while `process` worked.
676pub(crate) fn global_binding(name: &str) -> Option<Value> {
677    global_binding_from(name, false)
678}
679
680/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
681///
682/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
683/// which is what makes `globalThis.Math` and `globalThis.process` work — but
684/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
685/// running its locals were readable off `globalThis`: `function f() { let zzq =
686/// 2; return typeof globalThis.zzq }` answered for a name the global object has
687/// never heard of. Only the globals map and the lazy builtins below may answer
688/// here.
689pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
690    global_binding_from(name, true)
691}
692
693fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
694    let bound = with_host(|h| {
695        if object_only {
696            h.read_global(name)
697        } else {
698            h.read_name(name)
699        }
700    });
701    if let Some(v) = bound {
702        return Some(v);
703    }
704    // Globals bound lazily: numeric sentinels + builtin namespaces.
705    match name {
706        "undefined" => return Some(Value::Undef),
707        "NaN" => return Some(Value::Float(f64::NAN)),
708        "Infinity" => return Some(Value::Float(f64::INFINITY)),
709        // One object, not a fresh one per read: `globalThis === globalThis` is
710        // `true` in JS, and `globalThis.x = 1` is readable back as
711        // `globalThis.x`. Both were false while each read minted a new object.
712        // `global` is Node's alias for the same object.
713        "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
714        // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
715        // node-flavoured module: `globalThis.crypto.randomUUID` exists while
716        // `globalThis.crypto.createHash` does not.
717        "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
718        _ => {}
719    }
720    if is_namespace(name) || is_known_builtin(name) {
721        return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
722    }
723    None
724}
725
726fn b_getlocal(vm: &mut VM, _: u8) -> Value {
727    let name = sname(&vm.pop());
728    // A module-top-level dead zone is tracked by NAME rather than by a parked
729    // marker, so that the marker is never reachable as `globalThis.<name>`. It
730    // only applies when nothing on the scope chain SHADOWS the name — a class's
731    // own inner binding for its name does exactly that while its static
732    // initializers run.
733    if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
734        return abort(vm, host::tdz_error(&name));
735    }
736    match global_binding(&name) {
737        // The binding EXISTS but has not reached its declaration yet.
738        Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
739        Some(v) => v,
740        None => abort(vm, host::ref_error(&name)),
741    }
742}
743
744/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
745/// top of the scope that declares it.
746fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
747    let name = sname(&vm.pop());
748    with_host(|h| h.hoist_tdz(&name));
749    Value::Undef
750}
751
752/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
753/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
754/// code — and, either way, never rebinds the name.
755const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
756
757fn readonly_global_error(name: &str) -> String {
758    host::type_error(&format!(
759        "Cannot assign to read only property '{name}' of object '#<Object>'"
760    ))
761}
762
763fn b_setlocal(vm: &mut VM, _: u8) -> Value {
764    let val = vm.pop();
765    let name = sname(&vm.pop());
766    // Sloppy assignment to a non-writable global is DISCARDED, not applied:
767    // `undefined = 1` used to rebind the name and make every later `undefined`
768    // read back as `1`.
769    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
770        return val;
771    }
772    // Assigning to a binding still in its temporal dead zone throws too —
773    // `{ x = 1; let x }` is a ReferenceError, not an initialization.
774    if with_host(|h| match h.read_name(&name) {
775        Some(v) => h.is_tdz(&v),
776        None => h.is_tdz_global(&name),
777    }) {
778        return abort(vm, host::tdz_error(&name));
779    }
780    // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
781    // immutable binding). This used to succeed silently.
782    if !with_host(|h| h.set_name(&name, val.clone())) {
783        return abort(vm, host::type_error("Assignment to constant variable."));
784    }
785    val
786}
787
788/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
789/// where sloppy code silently creates a global, strict code throws
790/// `ReferenceError: x is not defined`.
791///
792/// A separate opcode rather than a runtime flag: strictness is a static property
793/// of the code, so the compiler already knows which of the two an assignment is
794/// and sloppy code — everything in a CommonJS module without the directive —
795/// keeps the exact instruction it had.
796fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
797    let val = vm.pop();
798    let name = sname(&vm.pop());
799    if !binding_exists(&name) {
800        return abort(vm, host::ref_error(&name));
801    }
802    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
803        return abort(vm, readonly_global_error(&name));
804    }
805    if !with_host(|h| h.set_name(&name, val.clone())) {
806        return abort(vm, host::type_error("Assignment to constant variable."));
807    }
808    val
809}
810
811/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
812/// materialised builtin namespace. `global_binding` answers the same question
813/// but ALLOCATES the namespace object to do it, which an assignment then throws
814/// away.
815fn binding_exists(name: &str) -> bool {
816    if with_host(|h| h.has_name(name)) {
817        return true;
818    }
819    matches!(
820        name,
821        "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
822    ) || is_namespace(name)
823        || is_known_builtin(name)
824}
825
826fn b_declare(vm: &mut VM, _: u8) -> Value {
827    let val = vm.pop();
828    let name = sname(&vm.pop());
829    with_host(|h| h.declare_name(&name, val.clone()));
830    val
831}
832
833/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
834/// assignment to the name throws instead of overwriting it.
835fn b_declare_const(vm: &mut VM, _: u8) -> Value {
836    let val = vm.pop();
837    let name = sname(&vm.pop());
838    with_host(|h| h.declare_const_name(&name, val.clone()));
839    val
840}
841
842/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
843/// open block scopes, so the name outlives the block it was written in.
844/// `var` hoisting: create the binding as `undefined` unless it already exists.
845fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
846    let name = sname(&vm.pop());
847    with_host(|h| h.hoist_var_name(&name));
848    Value::Undef
849}
850
851fn b_declare_var(vm: &mut VM, _: u8) -> Value {
852    let val = vm.pop();
853    let name = sname(&vm.pop());
854    with_host(|h| h.declare_var_name(&name, val.clone()));
855    val
856}
857
858fn b_push_scope(_: &mut VM, _: u8) -> Value {
859    with_host(|h| h.push_scope());
860    Value::Undef
861}
862
863fn b_pop_scope(_: &mut VM, _: u8) -> Value {
864    with_host(|h| h.pop_scope());
865    Value::Undef
866}
867
868fn b_copy_scope(_: &mut VM, _: u8) -> Value {
869    with_host(|h| h.copy_scope());
870    Value::Undef
871}
872
873fn b_delname(vm: &mut VM, _: u8) -> Value {
874    let name = sval(&vm.pop());
875    with_host(|h| h.del_name(&name));
876    Value::Bool(true)
877}
878
879fn b_this(vm: &mut VM, _: u8) -> Value {
880    if with_host(|h| h.this_state()) == host::ThisState::Pending {
881        return abort(vm, host::this_before_super_error());
882    }
883    with_host(|h| h.current_this().unwrap_or(Value::Undef))
884}
885
886fn b_load_null(_vm: &mut VM, _: u8) -> Value {
887    with_host(|h| h.null())
888}
889
890// ── attribute / item handlers ─────────────────────────────────────────────────
891
892fn b_getattr(vm: &mut VM, _: u8) -> Value {
893    let name = sval(&vm.pop());
894    let recv = vm.pop();
895    match get_property(&recv, &name) {
896        Ok(v) => v,
897        Err(e) => abort(vm, e),
898    }
899}
900
901/// Read `recv.name` (also the computed-key path for string keys). Walks own
902/// properties, accessors, and the prototype chain (class methods / getters).
903/// Read one small piece out of `recv`'s heap cell under a short borrow.
904///
905/// The closure must not call back into the host (`with_host` is a `RefCell`
906/// borrow and re-entering panics) — which is exactly why it hands back only the
907/// value needed: the caller re-enters freely afterwards. This replaces the old
908/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
909/// `String` just to look at it.
910fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
911    with_host(|h| h.get(recv).and_then(f))
912}
913
914/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
915/// reaches it without a closer link already owning `name`.
916///
917/// A proxy prototype answers only from the position it occupies in the chain: a
918/// nearer prototype that owns the key (as a data property or an accessor) still
919/// wins, exactly as `OrdinaryGet` walks one link at a time.
920pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
921    with_host(|h| {
922        let mut cur = h.proto_of(recv);
923        for _ in 0..100 {
924            let p = cur?;
925            match h.get(&p) {
926                Some(JsObj::Proxy { .. }) => return Some(p),
927                Some(JsObj::Object(props)) if props.contains_key(name) => return None,
928                _ => {}
929            }
930            if h.own_accessor(&p, name).is_some() {
931                return None;
932            }
933            cur = h.proto_of(&p);
934        }
935        None
936    })
937}
938
939/// The CommonJS wrapper's parameters. They are function locals in Node, not
940/// global-object properties, so `globalThis.require` is `undefined` and
941/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
942/// even though the bare `require` and `module` both work.
943const CJS_WRAPPER_LOCALS: &[&str] = &[
944    "require",
945    "module",
946    "exports",
947    "__filename",
948    "__dirname",
949    "__cjs_require",
950    "__cjs_resolve",
951];
952
953/// The globals node exposes as ENUMERABLE own properties of the global object —
954/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
955/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
956const ENUMERABLE_GLOBALS: &[&str] = &[
957    "global",
958    "clearImmediate",
959    "setImmediate",
960    "clearInterval",
961    "clearTimeout",
962    "setInterval",
963    "setTimeout",
964    "queueMicrotask",
965    "structuredClone",
966    "atob",
967    "btoa",
968    "performance",
969    "fetch",
970    "crypto",
971    "navigator",
972    "sessionStorage",
973];
974
975pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
976    // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
977    // when the receiver carries no such private element — it does NOT read back
978    // as `undefined`, which is what `C.prototype.method.call({})` used to do.
979    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
980        return Err(private_brand_message(name, false));
981    }
982    get_property_recv(recv, name, recv)
983}
984
985/// The `TypeError` a failed private brand check raises. Node words it two ways:
986/// a private METHOD or accessor names the class the receiver should have been an
987/// instance of, while a private FIELD names the member.
988pub fn private_brand_message(name: &str, writing: bool) -> String {
989    if with_host(|h| h.is_private_method(name)) {
990        if let Some(class) = with_host(|h| h.current_home_class_name()) {
991            return host::type_error(&format!("Receiver must be an instance of class {class}"));
992        }
993    }
994    let verb = if writing { "write" } else { "read" };
995    let prep = if writing { "to" } else { "from" };
996    host::type_error(&format!(
997        "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
998    ))
999}
1000
1001/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1002/// from and is what a getter sees as `this`; it differs from `recv` only when the
1003/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1004/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1005/// passes `recv` itself.
1006/// Re-format an error's `.stack` header on its first read, the way V8 does.
1007///
1008/// The constructor could only stamp the name it was called with, so a subclass
1009/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1010/// before the first read — left a stale header. Node re-reads both properties at
1011/// format time, including one inherited from the prototype (`E.prototype.name`).
1012///
1013/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1014/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1015/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1016/// assignment is never clobbered by a later read.
1017/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1018/// the ordinary stack path can skip the hook round-trip when nothing custom is
1019/// installed.
1020pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1021
1022pub fn materialize_stack(recv: &Value) {
1023    let Some(frames) = with_host(|h| match h.get(recv) {
1024        Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1025        _ => None,
1026    }) else {
1027        return;
1028    };
1029    // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1030    // stack-introspection hook, which every source-map library installs). It was
1031    // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1032    // read bypassed it and handed back the default text.
1033    let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1034    if let Some(f) = prep.filter(|f| {
1035        // The default hook produces exactly what the fast path below produces,
1036        // so it is skipped rather than called.
1037        !matches!(
1038            with_host(|h| h.get(f).cloned()),
1039            Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1040        ) && matches!(
1041            with_host(|h| h.get(f).cloned()),
1042            Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1043        )
1044    }) {
1045        // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1046        // second materialization would re-enter this path forever.
1047        with_host(|h| {
1048            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1049                p.shift_remove("@@stackRaw");
1050            }
1051        });
1052        let limit = with_host(|h| h.stack_trace_limit());
1053        if let Ok(sites) = crate::module::callsite_stack(limit) {
1054            if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1055                with_host(|h| {
1056                    if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1057                        p.insert("stack".into(), out);
1058                    }
1059                });
1060                return;
1061            }
1062        }
1063    }
1064    with_host(|h| {
1065        let frames = h.str_of(&frames);
1066        let name = host::lookup_chain(h, recv, "name")
1067            .map(|v| h.str_of(&v))
1068            .unwrap_or_else(|| "Error".to_string());
1069        let message = host::lookup_chain(h, recv, "message")
1070            .map(|v| h.str_of(&v))
1071            .unwrap_or_default();
1072        let header = if message.is_empty() {
1073            name
1074        } else {
1075            format!("{name}: {message}")
1076        };
1077        let sv = h.new_str(format!("{header}{frames}"));
1078        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1079            p.insert("stack".into(), sv);
1080            p.shift_remove("@@stackRaw");
1081        }
1082    });
1083}
1084
1085pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1086    // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1087    // target. Checked before anything else so no ordinary-object shortcut can
1088    // read past the handler.
1089    if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1090        return Ok(v);
1091    }
1092    if with_host(|h| h.is_nullish(recv)) {
1093        return Err(host::type_error(&format!(
1094            "Cannot read properties of {} (reading '{name}')",
1095            with_host(|h| h.str_of(recv))
1096        )));
1097    }
1098    if name == "stack" {
1099        materialize_stack(recv);
1100    }
1101    // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1102    // internal slots, so they resolve here rather than out of a property map.
1103    if let Some(v) = dom_exception_slot(recv, name) {
1104        return Ok(v);
1105    }
1106    // A read off `globalThis` for a name the object does not own falls back to
1107    // the same lazy global binding the bare identifier gets. Without it the
1108    // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1109    // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1110    // `false` and any `globalThis.X` feature probe reported the feature missing.
1111    if with_host(|h| h.is_global_object(recv)) {
1112        let own = with_host(|h| match h.get(recv) {
1113            Some(JsObj::Object(p)) => p.contains_key(name),
1114            _ => false,
1115        });
1116        // The CommonJS wrapper's parameters are function locals in Node, not
1117        // global-object properties: `typeof globalThis.require` is `undefined`
1118        // there even though the bare `require` works.
1119        if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1120            if let Some(v) = global_object_binding(name) {
1121                return Ok(v);
1122            }
1123        }
1124    }
1125    // Accessor (own or inherited getter) takes precedence over the chain walk.
1126    // The getter runs with the RECEIVER as `this`, not the object that owns it.
1127    if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1128        return match getter {
1129            Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1130            None => Ok(Value::Undef), // set-only property reads as undefined
1131        };
1132    }
1133    // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1134    // one expose it to a plain read, not just to `Object.prototype.toString` —
1135    // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1136    // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1137    // Anything the receiver's own chain provides wins (a class may define its
1138    // own getter), so this is only the fallback.
1139    if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1140        if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1141            return Ok(with_host(|h| h.new_str(tag)));
1142        }
1143    }
1144    // `constructor`: a user class/function sets it on the prototype chain, and
1145    // that wins; otherwise every builtin instance reports its native
1146    // constructor (so `[].constructor`, `new Map().constructor`,
1147    // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1148    if name == "constructor" {
1149        if let Some(v) = with_host(|h| {
1150            match h.get(recv) {
1151                Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1152                _ => None,
1153            }
1154            .or_else(|| host::lookup_chain(h, recv, "constructor"))
1155        }) {
1156            return Ok(v);
1157        }
1158        // An intrinsic prototype the receiver's CHAIN reaches owns a
1159        // `constructor` too, and it wins over the receiver's own kind:
1160        // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1161        // Deciding from the kind alone also mis-named the receiver in every
1162        // message that renders one — the brand-check errors say `#<Map>`.
1163        if let Some(c) = chain_intrinsic_ctors(recv)
1164            .into_iter()
1165            .find(|c| is_builtin_ctor(c))
1166        {
1167            return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1168        }
1169        if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1170            return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1171        }
1172    }
1173    // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1174    // answers for EVERY object that inherits from it, not only plain ones —
1175    // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1176    // so an array, function or builtin instance read `undefined`. An object with
1177    // a null prototype inherits no such accessor and reads `undefined`, which is
1178    // why this is skipped there rather than answering `null`.
1179    if name == "__proto__"
1180        && !with_host(|h| h.has_null_proto(recv))
1181        && peek(recv, |o| match o {
1182            JsObj::Object(p) => Some(p.contains_key("__proto__")),
1183            _ => Some(false),
1184        }) != Some(true)
1185    {
1186        return Ok(prototype_of(recv));
1187    }
1188    // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1189    // method: it RUNS the getter with that prototype as `this`, and all but two
1190    // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1191    // answered `undefined`, so both the value and the failure were invisible.
1192    // Both representations of a prototype reach here — the namespace handles
1193    // and the real objects (`Symbol.prototype`, `String.prototype`).
1194    if let Some(ctor) = intrinsic_proto_of(recv) {
1195        if is_proto_accessor(&ctor, name) {
1196            return proto_getter_call(&ctor, name, recv);
1197        }
1198    }
1199    let kind = with_host(|h| h.kind_of(recv));
1200    #[allow(unused_mut)]
1201    let mut out = match kind {
1202        Some(ObjKind::Object) => {
1203            let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1204            // A view over a DETACHED buffer reports zero extent. Its own
1205            // `length`/`byteLength`/`byteOffset` properties still hold the old
1206            // numbers — the buffer does not know its views, so it cannot rewrite
1207            // them — and reading them straight back made a detached view still
1208            // look eight bytes long.
1209            if matches!(name, "length" | "byteLength" | "byteOffset")
1210                && crate::stdlib::typedarray::view_detached(recv)
1211            {
1212                match crate::stdlib::native_tag(recv).as_deref() {
1213                    Some("TypedArray") => return Ok(Value::Float(0.0)),
1214                    // A DataView THROWS where a typed array answers zero — its
1215                    // extent accessors are brand-checked and node reports the
1216                    // getter by name.
1217                    Some("DataView") => {
1218                        return Err(crate::stdlib::typedarray::detached_error(
1219                            "get DataView.prototype",
1220                            name,
1221                            false,
1222                        ))
1223                    }
1224                    _ => {}
1225                }
1226            }
1227            // Typed-array element read (`ta[i]`): elements live in a hidden
1228            // `@@elems`, not as own numeric props, so intercept integer keys.
1229            if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1230                if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1231                    return Ok(v);
1232                }
1233            }
1234            // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1235            // own numeric props, so integer keys read through to it.
1236            if numeric
1237                && peek(recv, |o| match o {
1238                    JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1239                    _ => None,
1240                })
1241                .unwrap_or(false)
1242            {
1243                return Ok(crate::stdlib::buffer::byte_get(recv, name));
1244            }
1245            if let Some(v) = peek(recv, |o| match o {
1246                JsObj::Object(p) => p.get(name).cloned(),
1247                _ => None,
1248            }) {
1249                v
1250            } else if let Some(link) = proxy_proto_link(recv, name) {
1251                // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1252                // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1253                // receiver, so the trap sees the child as `receiver` and `this`
1254                // inside a trap-served getter resolves to the child, not the
1255                // proxy. `lookup_chain` cannot do this: it reads property maps,
1256                // and a proxy has none.
1257                return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1258            } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1259                // A method / data property inherited from the prototype chain.
1260                v
1261            } else if crate::stdlib::native_tag(recv)
1262                .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1263                .unwrap_or(false)
1264            {
1265                // A native instance method read as a property (`server.listen`) →
1266                // a bound method, dispatched via `instance_call` when invoked.
1267                bound_method(recv, name)
1268            } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1269                // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1270                // read as `undefined` there — which is also what makes
1271                // `Object.create(null) + 1` the spec `TypeError` instead of a
1272                // silent `"[object Object]1"`.
1273                bound_method(recv, name)
1274            } else {
1275                Value::Undef
1276            }
1277        }
1278        Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1279            function_property(recv, name)
1280        }
1281        // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1282        // thunk here. It is a function value, so it answers the function
1283        // properties: `[].slice.name` was `undefined` where node reports
1284        // `slice`, and `String([].slice)` fell through to
1285        // `Object.prototype.toString`.
1286        Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1287        Some(ObjKind::Symbol) => match name {
1288            "description" => {
1289                match peek(recv, |o| match o {
1290                    JsObj::Symbol { desc, .. } => desc.clone(),
1291                    _ => None,
1292                }) {
1293                    Some(d) => with_host(|h| h.new_str(d)),
1294                    None => Value::Undef,
1295                }
1296            }
1297            "toString" => bound_method(recv, name),
1298            // Anything else a symbol answers, it inherits from
1299            // `Symbol.prototype`. The arm used to stop at `undefined`, so
1300            // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1301            // as absent even though the prototype defines both — a symbol is an
1302            // ordinary object for the purpose of a property LOOKUP, only its
1303            // methods are branded.
1304            _ => with_host(|h| {
1305                h.ensure_wrapper_protos();
1306                h.native_proto("Symbol")
1307            })
1308            .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1309            .unwrap_or(Value::Undef),
1310        },
1311        Some(ObjKind::BigInt) => {
1312            if matches!(
1313                name,
1314                "toString" | "valueOf" | "toLocaleString" | "constructor"
1315            ) {
1316                bound_method(recv, name)
1317            } else {
1318                Value::Undef
1319            }
1320        }
1321        Some(ObjKind::RegExp) => {
1322            // A RegExp holds no collection, so cloning the compiled pattern here
1323            // does not scale with any input size; `regexp_property` re-enters the
1324            // host to allocate `source`/`flags`, so it cannot run under a borrow.
1325            let r = peek(recv, |o| match o {
1326                JsObj::RegExp(r) => Some(r.clone()),
1327                _ => None,
1328            });
1329            match r {
1330                Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1331                    // An OWN property beats the prototype method of the same
1332                    // name, which is ordinary resolution order. It mattered once
1333                    // the symbol-keyed methods existed: `re[Symbol.match] =
1334                    // false` disowns the regexp label (7.2.8), and the method
1335                    // was shadowing the assignment so the value never took.
1336                    if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1337                        return v;
1338                    }
1339                    if crate::regexp::is_regexp_method(name) {
1340                        bound_method(recv, name)
1341                    } else {
1342                        Value::Undef
1343                    }
1344                }),
1345                None => Value::Undef,
1346            }
1347        }
1348        // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1349        // the read must be `undefined` rather than a live count.
1350        Some(ObjKind::Map) => {
1351            let (len, weak) = peek(recv, |o| match o {
1352                JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1353                _ => None,
1354            })
1355            .unwrap_or((0, false));
1356            match name {
1357                "size" if !weak => Value::Float(len as f64),
1358                "@@iterator" => bound_method(recv, name),
1359                _ if is_map_method(name) => bound_method(recv, name),
1360                _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1361            }
1362        }
1363        Some(ObjKind::Set) => {
1364            let (len, weak) = peek(recv, |o| match o {
1365                JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1366                _ => None,
1367            })
1368            .unwrap_or((0, false));
1369            match name {
1370                "size" if !weak => Value::Float(len as f64),
1371                "@@iterator" => bound_method(recv, name),
1372                _ if is_set_method(name) => bound_method(recv, name),
1373                _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1374            }
1375        }
1376        Some(ObjKind::Generator) => {
1377            // A generator IS its own iterator, so it answers for the matching
1378            // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1379            // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1380            // was `undefined` even though `for await` over it worked through a
1381            // different path.
1382            let want = if with_host(|h| h.is_async_gen_val(recv)) {
1383                "@@asyncIterator"
1384            } else {
1385                "@@iterator"
1386            };
1387            if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1388            {
1389                bound_method(recv, name)
1390            } else {
1391                with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1392            }
1393        }
1394        Some(ObjKind::Promise) => {
1395            if matches!(name, "then" | "catch" | "finally") {
1396                bound_method(recv, name)
1397            } else {
1398                with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1399            }
1400        }
1401        Some(ObjKind::Iter) => {
1402            if matches!(name, "next" | "return" | "@@iterator")
1403                || crate::stdlib::iterator::is_helper(name)
1404            {
1405                bound_method(recv, name)
1406            } else {
1407                with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1408            }
1409        }
1410        Some(ObjKind::Array) => {
1411            if name == "length" {
1412                let n = peek(recv, |o| match o {
1413                    JsObj::Array(items) => Some(items.len()),
1414                    _ => None,
1415                })
1416                .unwrap_or(0);
1417                Value::Float(n as f64)
1418            } else if let Ok(i) = name.parse::<usize>() {
1419                peek(recv, |o| match o {
1420                    JsObj::Array(items) => items.get(i).cloned(),
1421                    _ => None,
1422                })
1423                // An index PAST an `arguments` object's length is an ordinary
1424                // own property in the side table, since adding one must not
1425                // move `length`. The array read alone could not see it, so the
1426                // write was invisible to every later read.
1427                .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1428                .unwrap_or(Value::Undef)
1429            } else if name == "@@iterator"
1430                || is_object_method(name)
1431                // An `arguments` object is array-BACKED here but is not an
1432                // Array: node's exposes no `Array.prototype` method, which is
1433                // exactly why the idiom is `Array.prototype.slice.call(args)`.
1434                // Exposing them made `arguments.map` a function.
1435                || (is_array_method(name) && !is_arguments(recv))
1436            {
1437                bound_method(recv, name)
1438            } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1439                // Extra own props attached to an array (e.g. `RegExp.exec` result's
1440                // `.index`/`.input`/`.groups`).
1441                v
1442            } else {
1443                Value::Undef
1444            }
1445        }
1446        Some(ObjKind::Str) => {
1447            // `.length` and `s[i]` count UTF-16 code units, not code points.
1448            if name == "length" {
1449                let n = peek(recv, |o| match o {
1450                    JsObj::Str(s) => Some(crate::utf16::len(s)),
1451                    _ => None,
1452                })
1453                .unwrap_or(0);
1454                Value::Float(n as f64)
1455            } else if let Ok(i) = name.parse::<usize>() {
1456                match peek(recv, |o| match o {
1457                    JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1458                    _ => None,
1459                }) {
1460                    Some(c) => with_host(|h| h.new_str(c)),
1461                    None => Value::Undef,
1462                }
1463            } else if name == "@@iterator" || is_string_method(name) {
1464                bound_method(recv, name)
1465            } else {
1466                Value::Undef
1467            }
1468        }
1469        Some(ObjKind::Builtin) => {
1470            let ns = peek(recv, |o| match o {
1471                JsObj::Builtin(ns) => Some(ns.clone()),
1472                _ => None,
1473            })
1474            .unwrap_or_default();
1475            let v = namespace_property(&ns, name);
1476            // `Function.prototype`'s methods READ off a builtin function. The
1477            // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1478            // the read answered `undefined` — so `typeof Math.max.bind` was
1479            // `"undefined"`, and `String(Math.max)` found no `toString` to
1480            // invoke and fell back to `Object.prototype.toString`'s
1481            // `[object Function]` where node reports the native-code form.
1482            if matches!(v, Value::Undef)
1483                && is_function_method(name)
1484                && host::builtin_is_callable(&ns)
1485            {
1486                return Ok(bound_method(recv, name));
1487            }
1488            v
1489        }
1490        _ => {
1491            // Primitive numbers/booleans: method access -> bound method.
1492            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1493                bound_method(recv, name)
1494            } else {
1495                Value::Undef
1496            }
1497        }
1498    };
1499    // Every object INHERITS the `Object.prototype` methods, and each kind's
1500    // read arm above knows only its OWN. So `typeof new Map().toString`,
1501    // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1502    // answered `undefined` — for Map the CALL already worked, which is the
1503    // read and the dispatch disagreeing about the same method.
1504    //
1505    // Which prototype owns the name is decided by the same helper the `in`
1506    // operator uses, so the two cannot drift, and the result is the SHARED
1507    // intrinsic rather than a per-read thunk.
1508    // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1509    // accessor throws rather than answering, which is how a strict function
1510    // keeps its caller unreachable. It read back as `undefined` here, which a
1511    // feature probe reads as "not supported" rather than "forbidden".
1512    // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1513    // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1514    // and `arguments` are.
1515    if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1516        return Err(host::type_error(POISON_PILL));
1517    }
1518    if matches!(name, "caller" | "arguments")
1519        && matches!(
1520            with_host(|h| h.kind_of(recv)),
1521            Some(ObjKind::Func) | Some(ObjKind::Class)
1522        )
1523    {
1524        return poison_pill_read(recv);
1525    }
1526    // `arguments.callee` in SLOPPY code is the running function — the
1527    // pre-`class` self-reference idiom. It read back `undefined`.
1528    if name == "callee" && is_arguments(recv) {
1529        if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1530            return Ok(f);
1531        }
1532    }
1533    // A method SYNTHESIZED from the receiver's kind is only reachable while the
1534    // receiver's intrinsic prototype is still on its chain. `Object
1535    // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1536    // above answers from the kind alone and cannot know the link changed. Only
1537    // a synthesized value is dropped — the two shapes a method read produces —
1538    // and only when the receiver does not own the name itself.
1539    if matches!(
1540        with_host(|h| h.get(&out).cloned()),
1541        Some(JsObj::BoundMethod { .. })
1542    ) || matches!(
1543        with_host(|h| h.get(&out).cloned()),
1544        Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1545    ) {
1546        // The kind arms synthesize their OWN kind's methods, so that is the
1547        // prototype whose reachability decides. Clearing the value here lets
1548        // the `inherited_method_owner` fallback below re-supply the
1549        // `Object.prototype` form where one exists — which is why
1550        // `a.toString` stays a function after the link is replaced while
1551        // `a.join` does not.
1552        if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1553            out = Value::Undef;
1554        }
1555    }
1556    // A key the receiver does not OWN is looked up on its prototype chain. The
1557    // exotic arms above answer from their own storage and stop, so an array
1558    // given a prototype inherited nothing through a read: with
1559    // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1560    // index and at one past the end, while `1 in a` already answered true —
1561    // the two views of the same question disagreeing. An accessor was found
1562    // (`lookup_accessor` walks), so only DATA properties went missing.
1563    //
1564    // A plain object's arm already consults the chain, and an array with no
1565    // explicit prototype has no links to walk, so this changes neither.
1566    if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1567        if matches!(out, Value::Undef) {
1568            if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1569                return Ok(v);
1570            }
1571        }
1572        // Then a monkey-patched intrinsic prototype member, which shadows the
1573        // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1574        // BE `f`. An explicitly-set prototype above wins over it, as the chain
1575        // order requires.
1576        if let Some(v) = inherited_builtin_static(recv, name) {
1577            return Ok(v);
1578        }
1579    }
1580    if matches!(out, Value::Undef) && !name.starts_with('#') {
1581        if let Some(owner) = inherited_method_owner(recv, name) {
1582            // An INHERITED accessor runs, it does not hand back a thunk, and
1583            // its brand check is about the receiver's internal slot rather than
1584            // its chain — `Object.create(Map.prototype).size` throws in node
1585            // even though `Map.prototype` is right there above it. This
1586            // answered `undefined`, which is the value a real Map would never
1587            // give and a plain object should never reach.
1588            if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1589                return proto_getter_call(owner, name, recv);
1590            }
1591            let key = format!("@proto:{owner}:{name}");
1592            if builtin_meta(&key).is_some() {
1593                return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1594            }
1595            // A DATA member of the prototype — `Array.prototype[Symbol
1596            // .unscopables]` is an object, not a method, so it is in neither
1597            // function table. Read it off the prototype itself rather than
1598            // answering `undefined`: an instance inherits it.
1599            let v = namespace_property(&format!("{owner}.prototype"), name);
1600            if !matches!(v, Value::Undef) {
1601                return Ok(v);
1602            }
1603        }
1604    }
1605    Ok(out)
1606}
1607
1608/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1609/// object literal because the module cache is the single source of truth: a
1610/// populated copy would answer reads correctly and silently ignore a `delete`,
1611/// which is the operation the property exists for.
1612pub const REQUIRE_CACHE: &str = "__cjs_cache";
1613
1614/// The builtin constructor name for a value with no own/inherited `constructor`
1615/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1616/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1617/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1618/// whose `.constructor.name` is `""` in Node — not worth modelling).
1619fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1620    match h.get(recv) {
1621        Some(JsObj::Array(_)) => Some("Array"),
1622        Some(JsObj::Object(props)) => {
1623            // A native instance reports its own constructor, not Object — e.g.
1624            // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1625            // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1626            // the `@@native` tag off the already-borrowed host (calling
1627            // `native_tag`, which re-enters `with_host`, would double-borrow).
1628            match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1629                Some("Buffer") => Some("Buffer"),
1630                Some("URL") => Some("URL"),
1631                Some("Date") => Some("Date"),
1632                Some("WeakRef") => Some("WeakRef"),
1633                Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1634                Some("TextEncoder") => Some("TextEncoder"),
1635                Some("TextDecoder") => Some("TextDecoder"),
1636                Some("EventEmitter") => Some("EventEmitter"),
1637                Some("Timeout") => Some("Timeout"),
1638                Some("Immediate") => Some("Immediate"),
1639                _ => Some("Object"),
1640            }
1641        }
1642        Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1643        Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1644        Some(JsObj::Promise { .. }) => Some("Promise"),
1645        Some(JsObj::Str(_)) => Some("String"),
1646        Some(JsObj::Symbol { .. }) => Some("Symbol"),
1647        Some(JsObj::BigInt(_)) => Some("BigInt"),
1648        Some(JsObj::RegExp(_)) => Some("RegExp"),
1649        Some(JsObj::Iter { .. }) => Some("Iterator"),
1650        Some(JsObj::Func(f)) => {
1651            // A generator or async function is NOT an ordinary function: its
1652            // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1653            // variants'), and so is its `constructor`. All three reported plain
1654            // `Function`, so `g.constructor.name` was `Function` where node
1655            // says `GeneratorFunction`.
1656            Some(match h.funcs.get(f.def_id) {
1657                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1658                Some(d) if d.is_generator => "GeneratorFunction",
1659                Some(d) if d.is_async => "AsyncFunction",
1660                _ => "Function",
1661            })
1662        }
1663        Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1664        _ => match recv {
1665            Value::Float(_) | Value::Int(_) => Some("Number"),
1666            Value::Bool(_) => Some("Boolean"),
1667            _ => None,
1668        },
1669    }
1670}
1671
1672/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1673/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1674/// `process`), whose `.name` is `undefined` in Node.
1675///
1676/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1677/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1678/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1679/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1680/// so a subclass is its own species unless it overrides the getter.
1681fn has_species(name: &str) -> bool {
1682    matches!(
1683        name,
1684        "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1685    ) || crate::stdlib::typedarray::is_ctor(name)
1686}
1687
1688fn is_builtin_ctor(name: &str) -> bool {
1689    matches!(
1690        name,
1691        "Array"
1692            | "Object"
1693            | "Number"
1694            | "String"
1695            | "Boolean"
1696            | "Symbol"
1697            | "Function"
1698            | "Map"
1699            | "Set"
1700            | "WeakMap"
1701            | "WeakSet"
1702            | "Promise"
1703            | "BigInt"
1704            | "Iterator"
1705            | "RegExp"
1706            | "Date"
1707            | "ArrayBuffer"
1708            | "DataView"
1709            | "Uint8Array"
1710            | "Int8Array"
1711            | "Uint8ClampedArray"
1712            | "Int16Array"
1713            | "Uint16Array"
1714            | "Int32Array"
1715            | "Uint32Array"
1716            | "Float32Array"
1717            | "Float64Array"
1718            | "BigInt64Array"
1719            | "BigUint64Array"
1720            | "WeakRef"
1721            | "FinalizationRegistry"
1722            | "TextEncoder"
1723            | "TextDecoder"
1724            | "IncomingMessage"
1725            | "ServerResponse"
1726            | "EventEmitter"
1727            | "Buffer"
1728            | "URL"
1729            | "URLSearchParams"
1730            | "Timeout"
1731            | "Immediate"
1732    ) || host::ERROR_NAMES.contains(&name)
1733        // The stream base classes are constructors too, and `require('stream')`
1734        // IS `Stream`, so `require('stream').name` has to answer.
1735        || crate::stdlib::stream::is_class(name)
1736}
1737
1738/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1739/// thunk can look its `name`/`length` up in the same table a
1740/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1741/// builtin constructor to name (a native stdlib instance, whose methods are
1742/// node's own JS and have no specified arity).
1743fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1744    let ctor = with_host(|h| default_ctor_name(h, recv))?;
1745    Some(format!("@proto:{ctor}:{method}"))
1746}
1747
1748/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1749/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1750/// table knows the method, because inventing an arity is worse than the
1751/// `undefined` a caller can test for.
1752fn bound_method_property(recv: &Value, name: &str) -> Value {
1753    let method = peek(recv, |o| match o {
1754        JsObj::BoundMethod { name, .. } => Some(name.clone()),
1755        _ => None,
1756    })
1757    .unwrap_or_default();
1758    let key = peek(recv, |o| match o {
1759        JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1760        _ => None,
1761    })
1762    .and_then(|inner| bound_method_key(&inner, &method));
1763    let meta = key.as_deref().and_then(builtin_meta);
1764    match name {
1765        "name" => {
1766            let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1767            with_host(|h| h.new_str(n))
1768        }
1769        "length" => match meta {
1770            Some((_, len)) => Value::Float(len as f64),
1771            None => Value::Undef,
1772        },
1773        _ if is_function_method(name) => bound_method(recv, name),
1774        _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1775    }
1776}
1777
1778fn bound_method(recv: &Value, name: &str) -> Value {
1779    // An ECMAScript intrinsic is ONE function object shared by every instance:
1780    // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1781    // true. Reading one off an instance used to mint a fresh thunk bound to that
1782    // instance, so every such comparison answered false — and a detached method
1783    // kept working on the receiver it was read off, where node throws because it
1784    // has no `this` at all.
1785    if let Some(key) = bound_method_key(recv, name) {
1786        if builtin_meta(&key).is_some() {
1787            return with_host(|h| h.alloc(JsObj::Builtin(key)));
1788        }
1789    }
1790    with_host(|h| {
1791        h.alloc(JsObj::BoundMethod {
1792            recv: recv.clone(),
1793            name: name.to_string(),
1794        })
1795    })
1796}
1797
1798/// `Object.prototype` methods reachable on any object.
1799fn is_object_method(name: &str) -> bool {
1800    matches!(
1801        name,
1802        "hasOwnProperty"
1803            | "isPrototypeOf"
1804            | "propertyIsEnumerable"
1805            | "toString"
1806            | "toLocaleString"
1807            | "valueOf"
1808            | "constructor"
1809            | "__defineGetter__"
1810            | "__defineSetter__"
1811            | "__lookupGetter__"
1812            | "__lookupSetter__"
1813    )
1814}
1815
1816/// The `Object.prototype` methods installed as thunks on the real
1817/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1818/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1819pub const OBJECT_PROTO_METHODS: &[&str] = &[
1820    "hasOwnProperty",
1821    "isPrototypeOf",
1822    "propertyIsEnumerable",
1823    "toString",
1824    "toLocaleString",
1825    "valueOf",
1826    "__defineGetter__",
1827    "__defineSetter__",
1828    "__lookupGetter__",
1829    "__lookupSetter__",
1830];
1831
1832/// A typed array with elements cannot be frozen or sealed: its indices are
1833/// non-configurable by construction, so making them non-writable would violate
1834/// the invariant, and node refuses outright rather than half-applying it. An
1835/// EMPTY view and a `DataView` are both fine.
1836/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1837///
1838/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1839/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1840/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1841/// never saw the query.
1842fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1843    if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1844        return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1845    }
1846    // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1847    if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1848        return Ok(Value::Bool(false));
1849    }
1850    for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1851        let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1852            continue;
1853        };
1854        let flag = |name: &str| {
1855            with_host(|h| match h.get(&d) {
1856                Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1857                _ => false,
1858            })
1859        };
1860        let is_data = with_host(
1861            |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1862        );
1863        if flag("configurable") || (freeze && is_data && flag("writable")) {
1864            return Ok(Value::Bool(false));
1865        }
1866    }
1867    Ok(Value::Bool(true))
1868}
1869
1870/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1871/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1872/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1873/// OBJECT, so the handler never saw the operation and the target was untouched.
1874///
1875/// Returns false for a non-proxy, which takes the ordinary path.
1876fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1877    if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1878        return Ok(false);
1879    }
1880    if !crate::proxy::prevent_extensions(v)? {
1881        return Err(host::type_error("Object.freeze called on non-object"));
1882    }
1883    let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1884    for key in keys {
1885        // SEALING asks for no descriptor at all — it only strips
1886        // `configurable`, which is the same for a data property and an
1887        // accessor. FREEZING has to know which it is, because only a data
1888        // property has a `writable` to strip, and that is the one extra trap
1889        // call node makes.
1890        let accessor = if freeze {
1891            let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1892                continue;
1893            };
1894            with_host(
1895                |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1896            )
1897        } else {
1898            false
1899        };
1900        let desc = with_host(|h| {
1901            let mut m: IndexMap<String, Value> = IndexMap::new();
1902            m.insert("configurable".into(), Value::Bool(false));
1903            if freeze && !accessor {
1904                m.insert("writable".into(), Value::Bool(false));
1905            }
1906            h.new_object(m)
1907        });
1908        if !crate::proxy::define_property(v, &key, &desc)? {
1909            return Err(host::type_error(&format!(
1910                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1911            )));
1912        }
1913    }
1914    Ok(true)
1915}
1916
1917fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1918    let has_elements = matches!(
1919        crate::stdlib::native_tag(v).as_deref(),
1920        Some("TypedArray") | Some("Buffer")
1921    ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1922    if has_elements {
1923        return Err(host::type_error(&format!(
1924            "Cannot {verb} array buffer views with elements"
1925        )));
1926    }
1927    Ok(())
1928}
1929
1930pub fn is_object_builtin_method(name: &str) -> bool {
1931    matches!(
1932        name,
1933        "hasOwnProperty"
1934            | "isPrototypeOf"
1935            | "propertyIsEnumerable"
1936            | "toString"
1937            | "toLocaleString"
1938            | "valueOf"
1939            | "__defineGetter__"
1940            | "__defineSetter__"
1941            | "__lookupGetter__"
1942            | "__lookupSetter__"
1943    )
1944}
1945
1946/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1947/// 20.1.3.6, the hook by which a class names its own brand.
1948///
1949/// A Proxy has no chain to probe: the step is an unconditional
1950/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1951/// ordinary receiver does, to keep the read off objects that carry no tag)
1952/// would always miss and brand every tagged proxy `[object Object]`.
1953///
1954/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1955fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1956    let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1957        || with_host(|h| {
1958            host::lookup_chain(h, recv, "@@toStringTag").is_some()
1959                || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1960        });
1961    if !tagged {
1962        return Ok(None);
1963    }
1964    let t = get_property(recv, "@@toStringTag")?;
1965    Ok(with_host(|h| h.as_str(&t)))
1966}
1967
1968/// Dispatch an `Object.prototype` builtin method on an object/instance.
1969pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1970    match name {
1971        // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1972        // reached by pre-`defineProperty` libraries; all four were missing, so
1973        // `o.__defineGetter__` threw "is not a function".
1974        "__defineGetter__" | "__defineSetter__" => {
1975            let getter = name == "__defineGetter__";
1976            let f = args.get(1).cloned().unwrap_or(Value::Undef);
1977            if !with_host(|h| host::is_callable(h, &f)) {
1978                return Err(host::type_error(&format!(
1979                    "Object.prototype.{name}: Expecting function"
1980                )));
1981            }
1982            let key = host::to_property_key(&arg0(&args))?;
1983            let desc = with_host(|h| {
1984                let mut m: IndexMap<String, Value> = IndexMap::new();
1985                m.insert(if getter { "get" } else { "set" }.into(), f);
1986                m.insert("enumerable".into(), Value::Bool(true));
1987                m.insert("configurable".into(), Value::Bool(true));
1988                h.new_object(m)
1989            });
1990            apply_descriptor(recv, &key, &desc)?;
1991            Ok(Value::Undef)
1992        }
1993        "__lookupGetter__" | "__lookupSetter__" => {
1994            let want_get = name == "__lookupGetter__";
1995            let key = host::to_property_key(&arg0(&args))?;
1996            // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
1997            let found = with_host(|h| host::lookup_accessor(h, recv, &key));
1998            Ok(match found {
1999                Some((g, st)) => {
2000                    let side = if want_get { g } else { st };
2001                    side.unwrap_or(Value::Undef)
2002                }
2003                None => Value::Undef,
2004            })
2005        }
2006        "hasOwnProperty" => {
2007            let k = host::to_property_key(&arg0(&args))?;
2008            // The global object OWNS its lazily-bound builtins and every global
2009            // a script created; neither lives in its property map.
2010            if with_host(|h| h.is_global_object(recv))
2011                && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2012                && global_object_binding(&k).is_some()
2013            {
2014                return Ok(Value::Bool(true));
2015            }
2016            // A builtin namespace/prototype receiver (`Map.prototype`) reports
2017            // ownership via `has_property` (its methods resolve as thunks).
2018            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2019                return Ok(Value::Bool(has_property(recv, &k)?));
2020            }
2021            // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2022            // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2023            // the target's property map.
2024            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2025                let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2026                return Ok(Value::Bool(!matches!(d, Value::Undef)));
2027            }
2028            // A Buffer's / typed array's own keys are its element indices: the
2029            // `length`/`byteLength` slots are internal bookkeeping, and V8
2030            // reports `hasOwnProperty('length')` as false for a typed array.
2031            // Shared with the `in` operator so the two cannot drift apart.
2032            if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2033                return Ok(Value::Bool(hit));
2034            }
2035            // A function's `length`/`name`/`prototype` and a RegExp's
2036            // `lastIndex` are SYNTHESIZED own properties: they read back but
2037            // own no map entry, so this answered false where node says true.
2038            if synthesized_own_descriptor(recv, &k).is_some() {
2039                return Ok(Value::Bool(true));
2040            }
2041            if uses_side_table(recv) {
2042                return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2043            }
2044            let has = with_host(|h| match h.get(recv) {
2045                Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2046                Some(JsObj::Array(items)) => {
2047                    k == "length"
2048                        || k.parse::<usize>()
2049                            .map(|i| i < items.len() && !h.is_hole(recv, i))
2050                            .unwrap_or(false)
2051                }
2052                _ => false,
2053            });
2054            Ok(Value::Bool(has))
2055        }
2056        "isPrototypeOf" => {
2057            let target = arg0(&args);
2058            // The ARGUMENT is what gets walked, so a proxy there needs its
2059            // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2060            // proxy does not hold, which reported `false` for every proxy. From
2061            // the second hop on the chain is ordinary objects again, walked by
2062            // the recorded link exactly as before.
2063            let mut cur = match crate::proxy::get_prototype_of(&target)? {
2064                Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
2065                None => with_host(|h| h.proto_of(&target)),
2066            };
2067            while let Some(p) = cur {
2068                if with_host(|h| h.strict_eq(&p, recv)) {
2069                    return Ok(Value::Bool(true));
2070                }
2071                cur = with_host(|h| h.proto_of(&p));
2072            }
2073            Ok(Value::Bool(false))
2074        }
2075        "propertyIsEnumerable" => {
2076            let k = with_host(|h| h.str_of(&arg0(&args)));
2077            // Own *and* enumerable — a non-enumerable own slot reads false. On a
2078            // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2079            // trap, since there is no property map to enumerate.
2080            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2081                let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2082                return Ok(Value::Bool(has));
2083            }
2084            let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2085            Ok(Value::Bool(has))
2086        }
2087        "toString" => {
2088            // An instance with a custom `toString` up the chain is handled by
2089            // call_method before reaching here; this is the default — and the
2090            // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2091            // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2092            // tagged object branded itself `[object T]` when asked one way and
2093            // `[object Object]` when converted the other (`String(o)`, `${o}`,
2094            // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2095            if let Some(t) = to_string_tag(recv)? {
2096                return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2097            }
2098            Ok(with_host(|h| {
2099                let s = h.str_of(recv);
2100                h.new_str(s)
2101            }))
2102        }
2103        // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2104        // `Invoke(this, "toString")` — no locale behavior of its own. It was
2105        // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2106        // calling it threw `is not a function` on every plain object.
2107        "toLocaleString" => {
2108            let v = host::call_method(recv, "toString", Vec::new())?;
2109            Ok(v)
2110        }
2111        "valueOf" => Ok(recv.clone()),
2112        _ => Err(host::type_error(&format!("{name} is not a function"))),
2113    }
2114}
2115
2116/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2117/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2118/// these (so the caller can try statics).
2119pub fn function_builtin_method(
2120    recv: &Value,
2121    name: &str,
2122    args: &[Value],
2123) -> Result<Option<Value>, String> {
2124    match name {
2125        "call" => {
2126            let this = args.first().cloned();
2127            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2128            Ok(Some(host::invoke(recv, rest, this)?))
2129        }
2130        "apply" => {
2131            let this = args.first().cloned();
2132            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2133            // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2134            // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2135            // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2136            // this is written for, and both produced an empty list. A nullish
2137            // second argument means no arguments at all.
2138            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2139                Vec::new()
2140            } else {
2141                create_list_from_array_like(&arr)?
2142            };
2143            Ok(Some(host::invoke(recv, call_args, this)?))
2144        }
2145        "bind" => {
2146            let this = args.first().cloned().unwrap_or(Value::Undef);
2147            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2148            Ok(Some(with_host(|h| {
2149                h.alloc(JsObj::BoundFunc {
2150                    target: recv.clone(),
2151                    this,
2152                    args: pre,
2153                })
2154            })))
2155        }
2156        "toString" => Ok(Some(with_host(|h| {
2157            let s = h.str_of(recv);
2158            h.new_str(s)
2159        }))),
2160        _ => Ok(None),
2161    }
2162}
2163
2164fn is_function_method(name: &str) -> bool {
2165    matches!(name, "call" | "apply" | "bind" | "toString")
2166}
2167fn is_map_method(name: &str) -> bool {
2168    matches!(
2169        name,
2170        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2171    )
2172}
2173fn is_set_method(name: &str) -> bool {
2174    matches!(
2175        name,
2176        "add"
2177            | "has"
2178            | "delete"
2179            | "clear"
2180            | "forEach"
2181            | "keys"
2182            | "values"
2183            | "entries"
2184            | "union"
2185            | "intersection"
2186            | "difference"
2187            | "symmetricDifference"
2188            | "isSubsetOf"
2189            | "isSupersetOf"
2190            | "isDisjointFrom"
2191    )
2192}
2193fn is_generator_method(name: &str) -> bool {
2194    matches!(name, "next" | "return" | "throw")
2195}
2196
2197/// A property read on a function/class value: own fn-props (statics, name,
2198/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2199fn function_property(recv: &Value, name: &str) -> Value {
2200    // A class static, inherited down the constructor chain.
2201    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2202        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2203            return v;
2204        }
2205        // A class's own `name` and `length` are its own, not the builtin
2206        // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2207        // `A.length === 0`, but both were read off `Array`. Only a class that
2208        // WOULD fall through to an ancestor takes this path; a plain class keeps
2209        // the ordinary computation below.
2210        if matches!(name, "name" | "length")
2211            && with_host(|h| h.class_static(recv, name)).is_none()
2212            && with_host(|h| h.class_builtin_ancestor(recv))
2213                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2214        {
2215            if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2216                return v;
2217            }
2218            if name == "name" {
2219                let n = with_host(|h| h.callable_name(recv));
2220                return with_host(|h| h.new_str(n));
2221            }
2222            // The class's own constructor decides its arity; with no explicit
2223            // one the implicit `constructor(...args)` has length 0.
2224            let ctor = with_host(|h| match h.get(recv) {
2225                Some(JsObj::Class(c)) => c.ctor.clone(),
2226                _ => None,
2227            });
2228            return match ctor {
2229                Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2230                None => Value::Float(0.0),
2231            };
2232        }
2233        // `Symbol.species` is an accessor returning `this`, so a subclass that
2234        // does not override it IS its own species. Reading it off the builtin
2235        // ancestor below would answer with the ancestor — `A[Symbol.species]`
2236        // came back as `Array`, which sent every derived result to a plain
2237        // array.
2238        if name == "@@species"
2239            && with_host(|h| h.class_static(recv, "@@species")).is_none()
2240            && with_host(|h| h.class_builtin_ancestor(recv))
2241                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2242        {
2243            return recv.clone();
2244        }
2245        // The chain may bottom out in a BUILTIN constructor (`class D extends
2246        // Array {}`), whose statics `class_static` cannot see — it only walks
2247        // `ClassVal.parent` links between user classes. Finish the lookup with an
2248        // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2249        if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2250            if let Ok(v) = get_property(&anc, name) {
2251                if !matches!(v, Value::Undef) {
2252                    return v;
2253                }
2254            }
2255        }
2256    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2257        return v;
2258    }
2259    // A method inherited via the function's [[Prototype]] chain (set with
2260    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2261    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2262    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2263        return v;
2264    }
2265    match name {
2266        "name" => with_host(|h| {
2267            let n = h.callable_name(recv);
2268            h.new_str(n)
2269        }),
2270        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2271        "prototype" => ensure_fn_prototype(recv),
2272        _ if is_function_method(name) => bound_method(recv, name),
2273        _ => Value::Undef,
2274    }
2275}
2276
2277/// The `.prototype` of a function value, auto-created on first access (as Node
2278/// does for every non-arrow function) with `.constructor` linking back. Arrow
2279/// functions have no `prototype`.
2280fn ensure_fn_prototype(recv: &Value) -> Value {
2281    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2282        return p;
2283    }
2284    // Only a constructor gets one: an arrow, a method definition and an async
2285    // function are not constructors, and a class sets its own (10.2.5).
2286    if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2287        return Value::Undef;
2288    }
2289    if !with_host(|h| h.owns_prototype(recv)) {
2290        return Value::Undef;
2291    }
2292    with_host(|h| {
2293        let proto = h.new_object(IndexMap::new());
2294        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2295            p.insert("constructor".to_string(), recv.clone());
2296        }
2297        h.hide_prop(&proto, "constructor");
2298        h.set_fn_prop(recv, "prototype", proto.clone());
2299        proto
2300    })
2301}
2302
2303/// The numeric constants a core namespace owns, in the order node reports them
2304/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2305/// list: the enumeration and the read have to agree, and they did not — every
2306/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2307/// all eight of Math's, so a member that plainly exists was invisible to any
2308/// reflective copy of the namespace.
2309///
2310/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2311/// is what separates them from the methods alongside them.
2312pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2313    const MATH: &[(&str, f64)] = &[
2314        ("E", std::f64::consts::E),
2315        ("LN10", std::f64::consts::LN_10),
2316        ("LN2", std::f64::consts::LN_2),
2317        ("LOG10E", std::f64::consts::LOG10_E),
2318        ("LOG2E", std::f64::consts::LOG2_E),
2319        ("PI", std::f64::consts::PI),
2320        ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2321        ("SQRT2", std::f64::consts::SQRT_2),
2322    ];
2323    const NUMBER: &[(&str, f64)] = &[
2324        ("MAX_VALUE", f64::MAX),
2325        // The smallest positive value a Number can hold, which is the
2326        // smallest SUBNORMAL double (`5e-324`), not Rust's
2327        // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2328        // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2329        // large.
2330        // The literal, not `f64::from_bits(1)`: that is only const-callable from
2331        // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2332        // bit pattern — the smallest positive subnormal.
2333        ("MIN_VALUE", 5e-324),
2334        ("NaN", f64::NAN),
2335        ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2336        ("POSITIVE_INFINITY", f64::INFINITY),
2337        ("MAX_SAFE_INTEGER", 9007199254740991.0),
2338        ("MIN_SAFE_INTEGER", -9007199254740991.0),
2339        ("EPSILON", f64::EPSILON),
2340    ];
2341    match ns {
2342        "Math" => MATH,
2343        "Number" => NUMBER,
2344        _ => &[],
2345    }
2346}
2347
2348/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2349/// measured on node v26.8.1:
2350///
2351/// ```text
2352/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype   w=false e=false c=false
2353/// Math.max.name, Math.max.length                       w=false e=false c=true
2354/// Math.floor, Array.from, Array.prototype.slice        w=true  e=false c=true
2355/// require('path').join                                 w=true  e=true  c=true
2356/// ```
2357///
2358/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2359/// `name`/`length` is read-only but configurable, and everything else is an
2360/// ordinary method — enumerable exactly when the namespace enumerates it, which
2361/// is what separates a core module's exports from an ECMAScript namespace's.
2362fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2363    let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2364        || key == "prototype"
2365        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2366    let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2367    // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2368    // data property, whatever the namespace's built-in members look like — the
2369    // synthesized answer reported it non-enumerable, so a monkey-patched member
2370    // described itself as one of the intrinsics.
2371    let assigned = !intrinsic_proto_member(ns, key)
2372        && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2373        && with_host(|h| h.builtin_static(ns, key).is_some());
2374    let enumerable = assigned
2375        || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2376    with_host(|h| {
2377        let mut m: IndexMap<String, Value> = IndexMap::new();
2378        m.insert("value".into(), value);
2379        m.insert(
2380            "writable".into(),
2381            Value::Bool(assigned || (!frozen && !own_fn_meta)),
2382        );
2383        m.insert("enumerable".into(), Value::Bool(enumerable));
2384        m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2385        h.new_object(m)
2386    })
2387}
2388
2389/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2390/// [`builtin_member_descriptor`], split out so `delete` can ask without
2391/// building a descriptor object.
2392/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2393/// really defines — as opposed to a name a script added. An assignment over one
2394/// of these is a `[[Set]]` and leaves its attributes alone.
2395fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2396    intrinsic_proto_members(ns).is_some_and(|members| {
2397        members
2398            .iter()
2399            .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2400    })
2401}
2402
2403fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2404    !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2405        || key == "prototype"
2406        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2407}
2408
2409/// The value of `<ns>.<name>` when it is one of those constants.
2410fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2411    namespace_constants(ns)
2412        .iter()
2413        .find(|(k, _)| *k == name)
2414        .map(|(_, v)| *v)
2415}
2416
2417/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2418/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2419/// ECMAScript builtin defines. The generated member table records the same
2420/// distinction with its `+` prefix.
2421fn is_webidl_proto(ctor: &str) -> bool {
2422    intrinsic_proto_members(&format!("{ctor}.prototype"))
2423        .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2424}
2425
2426/// The intrinsic constructor a value's own kind implies — the prototype it
2427/// inherits with no explicit link.
2428pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2429    default_ctor_name(h, v)
2430}
2431
2432/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2433/// an object inheriting it refuses an assignment to that name.
2434pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2435    crate::arity::PROTO_READONLY
2436        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2437        .ok()
2438        .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2439}
2440
2441/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2442/// property or a method.
2443pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2444    crate::arity::PROTO_ACCESSORS
2445        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2446        .ok()
2447        .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2448}
2449
2450/// The constructor whose `.prototype` IS `recv`, whichever of the two
2451/// representations it uses — a `Builtin` namespace handle or a real object.
2452pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2453    with_host(|h| match h.get(recv) {
2454        Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2455        _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2456    })
2457}
2458
2459/// The getter function of an intrinsic prototype accessor, as a first-class
2460/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2461/// hands back, and the form a library uses to borrow one.
2462fn proto_getter(ctor: &str, key: &str) -> Value {
2463    with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2464}
2465
2466/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2467/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2468/// in node even though `Map.prototype` is right there on the chain.
2469fn brand_matches(recv: &Value, ctor: &str) -> bool {
2470    if let Some(tag) = crate::stdlib::native_tag(recv) {
2471        if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2472            return true;
2473        }
2474    }
2475    match ctor {
2476        "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2477        "ArrayBuffer" => with_host(
2478            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2479        ),
2480        _ => {
2481            let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2482                Some(c) => Some(c),
2483                None => with_host(|h| default_ctor_name(h, recv)),
2484            };
2485            own == Some(ctor)
2486        }
2487    }
2488}
2489
2490thread_local! {
2491    /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2492    ///
2493    /// A getter's last step reads the value off the receiver, and when the
2494    /// receiver does not STORE it that read walks the chain, finds the same
2495    /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2496    /// stack overflowed and aborted the process. An accessor already in flight
2497    /// answers `undefined` for its own key rather than re-entering — the value
2498    /// a missing internal slot has, and what node reports for one.
2499    static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2500        const { std::cell::RefCell::new(Vec::new()) };
2501}
2502
2503/// Whether `ctor`'s `key` getter is already running further down the stack.
2504fn getter_in_flight(ctor: &str, key: &str) -> bool {
2505    GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2506}
2507
2508/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2509/// `@protoget:` thunks.
2510///
2511/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2512/// wrong: it answered `undefined` where node runs the getter, fails the brand
2513/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2514/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2515/// that would otherwise throw for every flag reads two of them back.
2516pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2517    let is_the_prototype = with_host(
2518        |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2519    );
2520    if is_the_prototype && ctor == "RegExp" {
2521        // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2522        // the getter returns rather than throwing. `source` and `flags` have
2523        // their own values there; every flag getter answers `undefined`.
2524        return Ok(match key {
2525            "source" => with_host(|h| h.new_str("(?:)".to_string())),
2526            "flags" => with_host(|h| h.new_str(String::new())),
2527            _ => Value::Undef,
2528        });
2529    }
2530    // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2531    // the individual flag properties off whatever object it is handed and
2532    // concatenates their letters, so a plain object answers `""` rather than
2533    // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2534    if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2535        if !with_host(|h| is_object_like(h, recv)) {
2536            return Err(regexp_brand_error(key, recv));
2537        }
2538        let mut out = String::new();
2539        for (prop, letter) in REGEXP_FLAG_LETTERS {
2540            let v = get_property(recv, prop)?;
2541            if with_host(|h| h.truthy(&v)) {
2542                out.push(*letter);
2543            }
2544        }
2545        return Ok(with_host(|h| h.new_str(out)));
2546    }
2547    // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2548    // the getter and the setter throw for every receiver, which is how a strict
2549    // function keeps its caller unreachable. They are not brand checks and do
2550    // not name the receiver.
2551    if ctor == "Function" && matches!(key, "arguments" | "caller") {
2552        return poison_pill_read(recv);
2553    }
2554    if !brand_matches(recv, ctor) {
2555        return Err(match ctor {
2556            "RegExp" => regexp_brand_error(key, recv),
2557            "Symbol" => {
2558                host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2559            }
2560            _ => host::type_error(&format!(
2561                "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2562                brand_receiver_string(recv)
2563            )),
2564        });
2565    }
2566    // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2567    // so that the public name can be a getter on the prototype rather than an
2568    // own enumerable property. Read it straight: the chain walk below would
2569    // find this same accessor and run it again.
2570    if let Some(v) = with_host(|h| match h.get(recv) {
2571        Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2572        _ => None,
2573    }) {
2574        return Ok(v);
2575    }
2576    GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2577    let out = get_property(recv, key);
2578    GETTERS_IN_FLIGHT.with(|g| {
2579        g.borrow_mut().pop();
2580    });
2581    out
2582}
2583
2584/// `Function.prototype.arguments`/`caller` read against `recv`.
2585///
2586/// The pill is conditional and the condition is the RECEIVER, not the reading
2587/// code: a sloppy non-arrow function answers `null` (node stopped populating
2588/// these long ago but kept them readable), and everything else — an arrow, a
2589/// strict function, a non-function — throws. Keying it on the READER's
2590/// strictness, which is what this did, made `strictFn.arguments` answer
2591/// `undefined` from sloppy code and a sloppy function throw from strict code:
2592/// wrong in both directions.
2593pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2594    if with_host(|h| h.fn_is_sloppy(recv)) {
2595        return Ok(with_host(|h| h.null()));
2596    }
2597    Err(host::type_error(POISON_PILL))
2598}
2599
2600/// The message both halves of the `arguments`/`caller` poison pill throw.
2601pub(crate) const POISON_PILL: &str = "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them";
2602
2603/// How a REJECTED receiver is rendered in a brand-check message.
2604///
2605/// `no_side_effects_string` answers for most of them, but two kinds differ:
2606/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2607/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2608/// that function therefore brands. Node draws the line at whether the value is
2609/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2610/// two cases here are the ones that fall on the other side of it.
2611fn brand_receiver_string(recv: &Value) -> String {
2612    if let Some(ctor) = intrinsic_proto_of(recv) {
2613        return format!("#<{ctor}>");
2614    }
2615    match crate::stdlib::native_tag(recv).as_deref() {
2616        Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2617        _ => no_side_effects_string(recv),
2618    }
2619}
2620
2621/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2622/// concatenates their letters.
2623const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2624    ("hasIndices", 'd'),
2625    ("global", 'g'),
2626    ("ignoreCase", 'i'),
2627    ("multiline", 'm'),
2628    ("dotAll", 's'),
2629    ("unicode", 'u'),
2630    ("unicodeSets", 'v'),
2631    ("sticky", 'y'),
2632];
2633
2634/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2635/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2636/// because 22.2.6.5 reads the individual flags off any object it is given.
2637fn regexp_brand_error(key: &str, recv: &Value) -> String {
2638    if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2639        return host::type_error(&format!(
2640            "RegExp.prototype.flags getter called on non-object {}",
2641            no_side_effects_string(recv)
2642        ));
2643    }
2644    host::type_error(&format!(
2645        "RegExp.prototype.{key} getter called on non-RegExp object"
2646    ))
2647}
2648
2649/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2650/// `console.log`).
2651pub fn namespace_property(ns: &str, name: &str) -> Value {
2652    // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2653    // read sees whatever is loaded now and `delete` (see `delete_property`)
2654    // actually invalidates.
2655    if ns == REQUIRE_CACHE {
2656        return crate::module::cache_get(name).unwrap_or(Value::Undef);
2657    }
2658    // A property a SCRIPT assigned onto this namespace wins over everything
2659    // synthesized below, including a member the namespace really has. That is
2660    // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2661    // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2662    // to read back at all. Only the two `Error` hooks consulted this table, so
2663    // every other assignment onto a builtin — the whole polyfill idiom — was
2664    // stored by `set_property` and then never read: the write appeared to
2665    // succeed, `Object.isExtensible` said true, and the value came back
2666    // `undefined`.
2667    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2668        return v;
2669    }
2670    // The ENTRY script's `require` is this builtin rather than the per-module
2671    // closure, so its `cache` has to be handed out here too.
2672    // `require.extensions` — the legacy loader map. Deprecated but still read
2673    // (and sometimes written) by tooling that hooks module loading, and it was
2674    // absent entirely. The three keys node ships are present; installing a
2675    // custom loader through them is NOT honoured by this runtime's loader, so
2676    // the map reports what it can serve rather than pretending otherwise.
2677    // `util.promisify.custom` — the registered symbol a module attaches to a
2678    // callback function to supply its own promisified form. It was `undefined`,
2679    // so the lookup that decides whether to use one always missed.
2680    if ns == "util.promisify" && name == "custom" {
2681        return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2682    }
2683    // `process.memoryUsage.rss()` — node's fast path for the one figure that
2684    // does not need the whole object built.
2685    if ns == "process.memoryUsage" && name == "rss" {
2686        return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2687    }
2688    if ns == "require" && name == "extensions" {
2689        return with_host(|h| {
2690            let mut m: IndexMap<String, Value> = IndexMap::new();
2691            for ext in [".js", ".json", ".node"] {
2692                let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2693                m.insert(ext.to_string(), f);
2694            }
2695            h.new_object(m)
2696        });
2697    }
2698    // `require.resolve.paths(spec)` — the directories a lookup would search:
2699    // `null` for a core module, the `node_modules` chain otherwise.
2700    if ns == "require.resolve" && name == "paths" {
2701        return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2702    }
2703    if ns == "require" && name == "cache" {
2704        return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2705    }
2706    // The legacy numeric codes `DOMException` carries as statics
2707    // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2708    if ns == "DOMException" {
2709        if let Some((_, code)) = DOM_EXCEPTION_CODES
2710            .iter()
2711            .find(|(n, _)| legacy_code_name(n) == name)
2712        {
2713            return Value::Float(*code);
2714        }
2715    }
2716    // Numeric constants.
2717    if let Some(k) = namespace_constant(ns, name) {
2718        return Value::Float(k);
2719    }
2720    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2721    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2722    // `undefined`.
2723    // `GeneratorFunction.prototype` and the two async variants are REAL objects
2724    // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2725    // sit on the prototype chain of every generator/async function, which a
2726    // handle cannot do. Without this the read fell through to `undefined`.
2727    if name == "prototype"
2728        && matches!(
2729            ns,
2730            "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2731        )
2732    {
2733        return with_host(|h| {
2734            h.ensure_native_protos();
2735            h.native_proto(ns).unwrap_or(Value::Undef)
2736        });
2737    }
2738    // `Error.prepareStackTrace` has a DEFAULT hook in node
2739    // (`ErrorPrepareStackTrace`), so a library probing `if
2740    // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2741    // down the wrong branch. The default renders the header plus the frames,
2742    // which is what the fast path in `materialize_stack` already produces — it
2743    // recognises this exact builtin and skips the round trip.
2744    if ns == "Error" && name == "prepareStackTrace" {
2745        return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2746            || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2747        );
2748    }
2749    // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2750    // lands in the builtin-static side table, which the read below consults
2751    // first. Without a default the READ was `undefined`, so a library doing
2752    // `const old = Error.stackTraceLimit` and restoring it later installed
2753    // `undefined` and disabled the limit permanently.
2754    if ns == "Error" && name == "stackTraceLimit" {
2755        return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2756            .unwrap_or(Value::Float(10.0));
2757    }
2758    // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2759    // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2760    // protocol had nothing to read and every derived result came back a plain
2761    // builtin.
2762    if name == "@@species" && has_species(ns) {
2763        return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2764    }
2765    if name == "name" && is_builtin_ctor(ns) {
2766        return with_host(|h| h.new_str(ns.to_string()));
2767    }
2768    // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2769    // computed property/method key.
2770    if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2771        return with_host(|h| h.well_known_symbol(name));
2772    }
2773    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2774    // `buffer.Buffer`, `url.URL`).
2775    if let Some(v) = crate::stdlib::constant(ns, name) {
2776        return v;
2777    }
2778    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2779    // `Array.prototype`, …): a prototype namespace whose methods are callable
2780    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2781    // `get-intrinsic`/`function-bind` family).
2782    if name == "prototype" && is_builtin_ctor(ns) {
2783        // Same reasoning as the native prototypes below, for the error
2784        // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2785        // `error_protos` object, so `Error.prototype` has to read back that same
2786        // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2787        // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2788        // Error("x")) === Error.prototype` was false, and `typeof
2789        // Error.prototype` was `"function"` where node says `"object"`.
2790        if host::ERROR_NAMES.contains(&ns) {
2791            if let Some(p) = with_host(|h| {
2792                h.ensure_error_protos();
2793                host::error_proto_of(h, ns)
2794            }) {
2795                return p;
2796            }
2797        }
2798        // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2799        // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2800        // Buffer.prototype` must compare equal, which a freshly-allocated
2801        // `Builtin` handle never can.
2802        if let Some(p) = with_host(|h| {
2803            h.ensure_native_protos();
2804            h.native_proto(ns)
2805        }) {
2806            return p;
2807        }
2808        let _ = ns;
2809        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2810    }
2811    // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2812    // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2813    // above never fired and the read produced `undefined` — which broke the ES5
2814    // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2815    // reads `StringDecoder.prototype.end` at load, and threw
2816    // `Cannot read properties of undefined (reading 'end')`. Built from the same
2817    // instance-method table a method read consults, so the two cannot disagree.
2818    if name == "prototype" {
2819        if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2820            return p;
2821        }
2822    }
2823    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2824    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2825    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2826    //
2827    // The thunk is minted only for a name the prototype REALLY carries. Minting
2828    // one unconditionally made every absent name answer with a function:
2829    // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2830    // says `undefined`, and so was every well-known symbol a prototype does not
2831    // define — `Array.prototype[Symbol.toStringTag]` came back a function
2832    // instead of `undefined`, which is a value `Object.prototype.toString` and
2833    // every `typeof`/truthiness test downstream then read wrong.
2834    //
2835    // Existence is decided by the generated intrinsic table, which is read out
2836    // of the reference engine, so this cannot drift from what node defines.
2837    // A name the prototype does not define but `Object.prototype` does is
2838    // INHERITED, and node hands back Object.prototype's own function object
2839    // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2840    // resolves to the `Object` thunk rather than a per-ctor one. That is also
2841    // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2842    // has no own `toString`, and the inherited one is the generic tag reader,
2843    // not a Map method that rejects a non-Map `this`.
2844    if let Some(ctor) = ns.strip_suffix(".prototype") {
2845        // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2846        // not an intrinsic function, so it is not in the arity table the lookup
2847        // above consults. It lists the methods a `with` block must NOT bring
2848        // into scope — the ones added after `with` existed, so old code using a
2849        // variable of the same name keeps working.
2850        if name == "@@unscopables" && ctor == "Array" {
2851            return with_host(|h| {
2852                let mut m: IndexMap<String, Value> = IndexMap::new();
2853                for k in [
2854                    "at",
2855                    "copyWithin",
2856                    "entries",
2857                    "fill",
2858                    "find",
2859                    "findIndex",
2860                    "findLast",
2861                    "findLastIndex",
2862                    "flat",
2863                    "flatMap",
2864                    "includes",
2865                    "keys",
2866                    "toReversed",
2867                    "toSorted",
2868                    "toSpliced",
2869                    "values",
2870                ] {
2871                    m.insert(k.to_string(), Value::Bool(true));
2872                }
2873                let o = h.new_object(m);
2874                let null = h.null();
2875                h.set_proto(&o, null);
2876                o
2877            });
2878        }
2879        if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2880            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2881        }
2882        if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2883            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2884        }
2885        // `constructor` is excluded from the table because it is not a method:
2886        // it is the constructor function itself, and node compares equal
2887        // (`Array.prototype.constructor === Array`). It used to resolve to a
2888        // `@proto:Array:constructor` thunk, which is a different object every
2889        // read and so never compared equal to anything.
2890        if name == "constructor" && is_builtin_ctor(ctor) {
2891            return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2892        }
2893        return Value::Undef;
2894    }
2895    let qualified = format!("{ns}.{name}");
2896    if is_known_builtin(&qualified) {
2897        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2898    }
2899    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2900    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2901        return v;
2902    }
2903    // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2904    // both). `Math.max.name` was `undefined` — as was every `.name` a library
2905    // reads to identify a callback it was handed. The non-callable namespaces
2906    // fall through: `Math.name` and `require('fs').length` really are undefined.
2907    if host::builtin_is_callable(ns) {
2908        match name {
2909            "name" => {
2910                if let Some(n) = proto_getter_name(ns) {
2911                    return with_host(|h| h.new_str(n));
2912                }
2913                return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2914            }
2915            // Only the intrinsics have a specified arity; a core-module
2916            // function's is a property of node's own JS source, so it stays
2917            // `undefined` rather than being invented here.
2918            "length" => {
2919                // A getter takes no argument (10.2.9 / the accessor grammar),
2920                // so its `length` is 0 — it is not in the intrinsic table,
2921                // which holds only named functions.
2922                if proto_getter_name(ns).is_some() {
2923                    return Value::Float(0.0);
2924                }
2925                if let Some((_, len)) = builtin_meta(ns) {
2926                    return Value::Float(len as f64);
2927                }
2928            }
2929            _ => {}
2930        }
2931    }
2932    Value::Undef
2933}
2934
2935/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2936/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2937/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2938/// libraries type-check on; every other method routes through normal method
2939/// dispatch on `recv`.
2940/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2941/// with NO receiver — `const f = [].push; f(1)`.
2942///
2943/// Reading a method off an instance used to mint a thunk bound to that
2944/// instance, so a detached method silently kept working on the object it came
2945/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2946/// to say so. Node words it four ways, and which one a method gets is not
2947/// something that can be derived — the split was measured across every method
2948/// of each prototype:
2949///
2950/// ```text
2951/// ToObject(this)         "Cannot convert undefined or null to object"
2952/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2953/// brand check            "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2954/// everything else        the generic incompatible-receiver message
2955/// ```
2956fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2957    // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2958    // the two `indexOf` family members name themselves, the rest go through
2959    // `ToObject` and report its message.
2960    const ARRAY_NAMED: &[&str] = &[
2961        "concat",
2962        "every",
2963        "filter",
2964        "find",
2965        "findIndex",
2966        "findLast",
2967        "findLastIndex",
2968        "forEach",
2969        "indexOf",
2970        "map",
2971        "reduce",
2972        "reduceRight",
2973        "some",
2974    ];
2975    const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2976    let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2977    let branded =
2978        |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2979    // The generic form names the receiver, so a `null` one must not be reported
2980    // as `undefined`.
2981    let generic = |c: &str, m: &str| {
2982        format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
2983    };
2984    Some(match ctor {
2985        "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
2986        "Array" => TO_OBJECT.to_string(),
2987        // `Object.prototype.toString` is the one method that ACCEPTS a nullish
2988        // receiver — it answers `[object Undefined]`.
2989        "Object" if method == "toString" => return None,
2990        "Object" if method == "toLocaleString" => named("Object"),
2991        "Object" => TO_OBJECT.to_string(),
2992        // Both aliases report the LEGACY name in the message, which is the one
2993        // place `name` and the message disagree.
2994        "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
2995        "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
2996        "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
2997        "String" => named("String"),
2998        "Number" => branded("Number", "Number"),
2999        "Boolean" => branded("Boolean", "Boolean"),
3000        "Symbol" => branded("Symbol", "Symbol"),
3001        "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3002        "Function" if matches!(method, "call" | "apply") => format!(
3003            "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3004        ),
3005        "Function" => branded("Function", "Function"),
3006        // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3007        // a nullish receiver fails inside them and reports that instead.
3008        "Promise" if method == "catch" => {
3009            "Cannot read properties of undefined (reading 'then')".to_string()
3010        }
3011        "Promise" if method == "finally" => {
3012            "Promise.prototype.finally called on non-object".to_string()
3013        }
3014        "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3015        // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3016        // report that slot check; every setter, every `to*String` and the two
3017        // legacy year methods go through the generic receiver check first.
3018        "Date"
3019            if method == "valueOf"
3020                || (method.starts_with("get") && method != "getYear") =>
3021        {
3022            "this is not a Date object.".to_string()
3023        }
3024        // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3025        // and `Set.prototype.keys` IS `values`, one function object each.
3026        "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3027        "Set" if method == "keys" => generic("Set", "values"),
3028        // Everything else that is brand-checked names itself. Node reaches this
3029        // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3030        // is the receiver's kind, and only the message has to agree.
3031        "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3032        | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3033        "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3034        // Node's `URL` methods fail while reaching for their internal state, and
3035        // report the read that failed rather than the method.
3036        "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3037        _ => return None,
3038    })
3039}
3040
3041/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3042/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3043/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3044/// are generic and coerce their receiver with `ToString`.
3045fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3046    match ctor {
3047        "Number" => matches!(
3048            method,
3049            "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3050        ),
3051        "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3052        "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3053        _ => false,
3054    }
3055}
3056
3057/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3058/// primitive itself, the primitive a wrapper boxes, or — for the three
3059/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3060/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3061fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3062    let expected = match ctor {
3063        "Number" => "number",
3064        "String" => "string",
3065        "Boolean" => "boolean",
3066        "BigInt" => "bigint",
3067        _ => return None,
3068    };
3069    let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3070    if is_expected(recv) {
3071        return Some(recv.clone());
3072    }
3073    if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3074        return Some(prim);
3075    }
3076    if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3077        return match ctor {
3078            "Number" => Some(Value::Float(0.0)),
3079            "String" => Some(with_host(|h| h.new_str(""))),
3080            "Boolean" => Some(Value::Bool(false)),
3081            _ => None,
3082        };
3083    }
3084    None
3085}
3086
3087pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3088    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3089    // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3090    // the instance's hidden `@@<name>` slot, which is where the value lives now
3091    // that the public name is a getter rather than an own property.
3092    if let Some(key) = method.strip_prefix("@get@") {
3093        if let Some(v) = with_host(|h| match h.get(recv) {
3094            Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3095            _ => None,
3096        }) {
3097            return Ok(v);
3098        }
3099        // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3100        // `symmetricKeySize` is the secret's byte length, which nothing stores.
3101        let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3102        return crate::stdlib::instance_call(&tag, recv, method, args);
3103    }
3104    if let Some(key) = method.strip_prefix("@set@") {
3105        let v = args.first().cloned().unwrap_or(Value::Undef);
3106        with_host(|h| {
3107            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3108                p.insert(format!("@@{key}"), v);
3109            }
3110        });
3111        crate::stdlib::instance_accessor_written(ctor, key, recv);
3112        return Ok(Value::Undef);
3113    }
3114    if with_host(|h| h.is_nullish(recv)) {
3115        let shown = if with_host(|h| h.is_null(recv)) {
3116            "null"
3117        } else {
3118            "undefined"
3119        };
3120        if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3121            return Err(format!("TypeError: {msg}"));
3122        }
3123    }
3124    // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3125    // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3126    if ctor == "Error" && method == "toString" {
3127        // A `DOMException` keeps its `name`/`message` in internal slots, so the
3128        // chain read below would find the class name on the prototype instead.
3129        if let Some(n) = dom_exception_slot(recv, "name") {
3130            let name = with_host(|h| h.str_of(&n));
3131            let msg = dom_exception_slot(recv, "message")
3132                .map(|m| with_host(|h| h.str_of(&m)))
3133                .unwrap_or_default();
3134            let s = if msg.is_empty() {
3135                name
3136            } else {
3137                format!("{name}: {msg}")
3138            };
3139            return Ok(with_host(|h| h.new_str(s)));
3140        }
3141        // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3142        // so a PROXY supplies them through its `get` trap. Reading the stored
3143        // ones first made `String(new Proxy(err, handler))` ignore the handler.
3144        let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3145        let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3146        let s = match stored.flatten() {
3147            Some(s) => s,
3148            None => {
3149                let read = |k: &str| -> Result<Option<String>, String> {
3150                    Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3151                };
3152                let name = read("name")?.unwrap_or_else(|| "Error".into());
3153                let msg = read("message")?.unwrap_or_default();
3154                if msg.is_empty() {
3155                    name
3156                } else {
3157                    format!("{name}: {msg}")
3158                }
3159            }
3160        };
3161        return Ok(with_host(|h| h.new_str(s)));
3162    }
3163    // The methods that read their receiver through `thisNumberValue` /
3164    // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3165    // primitive, its wrapper, or the prototype object (which carries the zero
3166    // value) — anything else is a TypeError naming the method. Unchecked,
3167    // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3168    // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3169    // recursed through the generic conversion until the stack overflowed.
3170    if is_brand_checked_primitive_method(ctor, method) {
3171        let Some(prim) = this_primitive_value(ctor, recv) else {
3172            return Err(format!(
3173                "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3174            ));
3175        };
3176        return host::call_method(&prim, method, args);
3177    }
3178    // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3179    // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3180    // arrives with an already-primitive receiver and needs no unwrapping.
3181    if matches!(ctor, "String" | "Number" | "Boolean") {
3182        let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3183        return host::call_method(&prim, method, args);
3184    }
3185    // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3186    // readily as the primitive, and neither was unwrapped here. A BigInt
3187    // wrapper's `valueOf` therefore re-entered the generic conversion, which
3188    // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3189    // until the stack overflowed and ABORTED the process, which no try/catch can
3190    // see. A Symbol wrapper failed the brand check below instead and reported
3191    // that `this` was not a Symbol, when it is one. Only a real wrapper is
3192    // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3193    // the brand check.
3194    if matches!(ctor, "Symbol" | "BigInt") {
3195        if let Some(prim) = wrapped_primitive(recv) {
3196            return host::call_method(&prim, method, args);
3197        }
3198    }
3199    if ctor == "Object" && method == "toString" {
3200        // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3201        // (own or inherited, data property or getter) replaces the builtin brand,
3202        // which is how a class advertises its own (`class C { get
3203        // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3204        // runs outside the host borrow so an accessor can be invoked.
3205        // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3206        // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3207        // the ordinary receiver does, to keep the read off objects that have no
3208        // tag) would always miss and brand every tagged proxy `[object Object]`.
3209        if let Some(s) = to_string_tag(recv)? {
3210            return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3211        }
3212        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3213    }
3214    // These thunks now live on the real `Object.prototype` object, i.e. on the
3215    // receiver's own chain — routing back through `call_method` would re-resolve
3216    // this very thunk and recurse.
3217    if ctor == "Object" && is_object_builtin_method(method) {
3218        return object_builtin_method(recv, method, args);
3219    }
3220    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3221    // emitter method directly against `recv` (routing back through `call_method`
3222    // would re-resolve the mixed-in thunk and recurse).
3223    if ctor == "EventEmitter" {
3224        return crate::stdlib::events::instance_call(recv, method, args);
3225    }
3226    // Same recursion hazard for the exotics with a real prototype object: the
3227    // thunk now lives ON the receiver's prototype chain, so `call_method` would
3228    // re-resolve this very thunk. Dispatch straight to the native instance
3229    // implementation when the receiver is in fact an instance of `ctor`.
3230    if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3231        return crate::stdlib::buffer::instance_call(recv, method, &args);
3232    }
3233    // The shared typed-array methods now live on the `%TypedArray%.prototype`
3234    // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3235    // appears for anything read directly off `Uint8Array.prototype`. Both
3236    // dispatch the same way, and both must bypass `call_method` or the thunk
3237    // would re-resolve itself off the receiver's chain and recurse.
3238    if ctor == "Uint8Array" || ctor == "TypedArray" {
3239        match crate::stdlib::native_tag(recv).as_deref() {
3240            Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3241            Some("TypedArray") => {
3242                return crate::stdlib::typedarray::instance_call(recv, method, &args)
3243            }
3244            _ => {}
3245        }
3246    }
3247    // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3248    // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3249    // `LengthOfArrayLike`), which is what makes
3250    // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3251    // is not an Array, so `call_method` would report the method missing.
3252    if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3253        return array_generic(recv, method, args);
3254    }
3255    // The general form of the two special cases above: a thunk taken off a native
3256    // constructor's real prototype, invoked with a receiver that IS an instance of
3257    // that constructor. Routing back through `call_method` would re-resolve this
3258    // very thunk off the receiver's own chain and recurse forever, which is why
3259    // each such prototype needed a hand-written bypass; now they all have one.
3260    // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3261    // through its chain, and requiring an exact tag match sent that call back
3262    // into `call_method`, which re-resolved this same thunk and recursed until
3263    // the stack overflowed.
3264    if let Some(tag) = crate::stdlib::native_tag(recv) {
3265        let mut c = Some(tag.as_str());
3266        while let Some(t) = c {
3267            if t == ctor {
3268                return crate::stdlib::instance_call(&tag, recv, method, args);
3269            }
3270            c = crate::stdlib::native_parent(t);
3271        }
3272    }
3273    // A BRANDED method reached with a receiver that has no such internal slot.
3274    // Every arm above dispatches a receiver that IS an instance, so arriving
3275    // here with one of these constructors means the brand check failed — the
3276    // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3277    // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3278    // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3279    // `union is not a function`, which says the method does not exist rather
3280    // than that the receiver is the wrong kind of object.
3281    // `Date.prototype`'s methods split in two: the ones that read the time value
3282    // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3283    // rest take the ordinary branded form. Measured on node v26.8.1:
3284    // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3285    // and `.setHours.call({})` the second.
3286    if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3287        const THIS_TIME_VALUE: &[&str] = &[
3288            "getTime",
3289            "valueOf",
3290            "getYear",
3291            "getFullYear",
3292            "getMonth",
3293            "getDate",
3294            "getDay",
3295            "getHours",
3296            "getMinutes",
3297            "getSeconds",
3298            "getMilliseconds",
3299            "getUTCFullYear",
3300            "getUTCMonth",
3301            "getUTCDate",
3302            "getUTCDay",
3303            "getUTCHours",
3304            "getUTCMinutes",
3305            "getUTCSeconds",
3306            "getUTCMilliseconds",
3307            "getTimezoneOffset",
3308        ];
3309        if THIS_TIME_VALUE.contains(&method) {
3310            return Err(host::type_error("this is not a Date object."));
3311        }
3312        // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3313        // receiver and INVOKES `toISOString` on it, so it fails on the missing
3314        // method rather than on a brand.
3315        if method != "toJSON" {
3316            return Err(host::type_error(&format!(
3317                "Method Date.prototype.{method} called on incompatible receiver {}",
3318                no_side_effects_string(recv)
3319            )));
3320        }
3321    }
3322    // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3323    // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3324    // check the receiver at the call boundary take the branded form. Measured
3325    // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3326    // genuinely generic (it is `Array.prototype.toString`) and never brands.
3327    if matches!(ctor, "TypedArray" | "Uint8Array")
3328        && !matches!(
3329            crate::stdlib::native_tag(recv).as_deref(),
3330            Some("TypedArray") | Some("Buffer")
3331        )
3332    {
3333        const BRANDED: &[&str] = &[
3334            "slice",
3335            "subarray",
3336            "join",
3337            "sort",
3338            "at",
3339            "toReversed",
3340            "toSorted",
3341            "toLocaleString",
3342        ];
3343        if BRANDED.contains(&method) {
3344            return Err(host::type_error(&format!(
3345                "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3346                no_side_effects_string(recv)
3347            )));
3348        }
3349        // The four base64/hex methods brand themselves against `Uint8Array`
3350        // specifically — a WRONG view is as incompatible as a plain object, and
3351        // the generic guard here cannot tell those apart.
3352        if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3353            return Err(host::type_error(&format!(
3354                "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3355                no_side_effects_string(recv)
3356            )));
3357        }
3358        if method != "toString" {
3359            return Err(host::type_error("this is not a typed array."));
3360        }
3361    }
3362    // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3363    // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3364    // that back to `call_method` read `call` off the proxy again, which
3365    // resolved to this same thunk, and recursed until the stack overflowed and
3366    // aborted the process. The three are defined on the callee alone, so they
3367    // run here: the proxy's `apply` trap (or its target) gets the call.
3368    // `toString` recursed the same way.
3369    if ctor == "Function"
3370        && matches!(method, "call" | "apply" | "bind" | "toString")
3371        && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3372    {
3373        let mut rest = args.into_iter();
3374        let this_arg = rest.next().unwrap_or(Value::Undef);
3375        match method {
3376            "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3377            "apply" => {
3378                let list = match rest.next() {
3379                    None | Some(Value::Undef) => Vec::new(),
3380                    Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3381                    Some(v) => create_list_from_array_like(&v)?,
3382                };
3383                return host::invoke(recv, list, Some(this_arg));
3384            }
3385            "bind" => {
3386                let target = recv.clone();
3387                let pre: Vec<Value> = rest.collect();
3388                return Ok(with_host(|h| {
3389                    h.alloc(JsObj::BoundFunc {
3390                        target,
3391                        this: this_arg,
3392                        args: pre,
3393                    })
3394                }));
3395            }
3396            // A proxy has no source text; V8 prints the native form for it.
3397            _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3398        }
3399    }
3400    // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3401    // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3402    // object. Without this check `Symbol.prototype.toString()` re-entered the
3403    // generic string conversion, which looked `toString` up again and called it
3404    // again — an infinite recursion that overflowed the stack and ABORTED the
3405    // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3406    // The wording is Symbol's own, not the "incompatible receiver" form the
3407    // collections use.
3408    if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3409        // A symbol-KEYED method is named in brackets rather than after a dot:
3410        // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3411        // That is the message `String(Symbol.prototype)` produces, since the
3412        // conversion reaches `@@toPrimitive` before it would reach `toString`.
3413        let named = match method.strip_prefix("@@") {
3414            Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3415            None => format!("Symbol.prototype.{method}"),
3416        };
3417        return Err(host::type_error(&format!(
3418            "{named} requires that 'this' be a Symbol"
3419        )));
3420    }
3421    if let Some(label) = branded_method_label(ctor, recv) {
3422        return Err(host::type_error(&format!(
3423            "Method {label}.prototype.{method} called on incompatible receiver {}",
3424            no_side_effects_string(recv)
3425        )));
3426    }
3427    host::call_method(recv, method, args)
3428}
3429
3430/// The name a branded prototype method reports itself under when its receiver
3431/// fails the brand check, or `None` when `ctor`'s methods are generic over
3432/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3433/// receiver really is an instance.
3434///
3435fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3436    let kind = with_host(|h| h.kind_of(recv));
3437    // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3438    // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3439    // even though both are `JsObj::Set` here.
3440    let weak = peek(recv, |o| match o {
3441        JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3442        _ => None,
3443    })
3444    .unwrap_or(false);
3445    let ok = match ctor {
3446        "Set" => kind == Some(ObjKind::Set) && !weak,
3447        "WeakSet" => kind == Some(ObjKind::Set) && weak,
3448        "Map" => kind == Some(ObjKind::Map) && !weak,
3449        "WeakMap" => kind == Some(ObjKind::Map) && weak,
3450        "Promise" => kind == Some(ObjKind::Promise),
3451        _ => return None,
3452    };
3453    if ok {
3454        return None;
3455    }
3456    Some(match ctor {
3457        "Set" => "Set",
3458        "WeakSet" => "WeakSet",
3459        "Map" => "Map",
3460        "WeakMap" => "WeakMap",
3461        _ => "Promise",
3462    })
3463}
3464
3465/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3466/// uses for a value it must not run user code on. Measured on node v26.8.1
3467/// through `Map.prototype.get.call(x)`:
3468///
3469/// ```text
3470/// 5 / 'str' / true / null / undefined / 9n   the value's own ToString
3471/// Symbol('s')                                Symbol(s)
3472/// function f(){}                             its source text
3473/// new Error('e')                             Error: e
3474/// {} / new (class A {})                      #<Object> / #<A>
3475/// new Map() / Promise.resolve()              #<Map> / #<Promise>
3476/// [] / new Date() / /re/ / new Uint8Array()  [object Array] / [object Date] / …
3477/// { toString() {} } / Object.create(null)    [object Object]
3478/// ```
3479///
3480/// The split is one test: a receiver whose `toString` is still
3481/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3482/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3483/// which is why an object with its own `toString` prints `[object Object]` and
3484/// not what that method returns.
3485fn no_side_effects_string(recv: &Value) -> String {
3486    if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3487        return with_host(|h| h.str_of(recv));
3488    }
3489    if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3490        return s;
3491    }
3492    // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3493    // rather than inside it.
3494    let native = crate::stdlib::native_tag(recv).is_some();
3495    let brands_itself = with_host(|h| {
3496        // `Object.prototype.toString` reaches every object as a thunk on the
3497        // real prototype object, so its presence proves nothing; only a
3498        // toString the receiver's chain OVERRIDES it with counts.
3499        let overridden = host::lookup_chain(h, recv, "toString")
3500            .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3501            .unwrap_or(false);
3502        native
3503            || overridden
3504            || h.has_null_proto(recv)
3505            || !matches!(
3506                h.kind_of(recv),
3507                Some(ObjKind::Object)
3508                    | Some(ObjKind::Map)
3509                    | Some(ObjKind::Set)
3510                    | Some(ObjKind::Promise)
3511            )
3512    });
3513    if brands_itself {
3514        return with_host(|h| object_tag(h, recv));
3515    }
3516    let ctor = get_property(recv, "constructor")
3517        .ok()
3518        .map(|c| with_host(|h| h.callable_name(&c)))
3519        .filter(|n| !n.is_empty())
3520        .unwrap_or_else(|| "Object".to_string());
3521    format!("#<{ctor}>")
3522}
3523
3524/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3525/// one, or `None` when reading that symbol must yield `undefined`.
3526///
3527/// Every builtin brand is already computed in exactly one place (`object_tag`),
3528/// so this reuses it and subtracts the legacy builtins, which brand for
3529/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3530/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3531/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3532/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3533/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3534/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3535/// `TextEncoder`/`TextDecoder` all read their brand.
3536pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3537    // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3538    // of which `object_tag` already brands.
3539    let tag = object_brand(h, v);
3540    const NO_TAG: &[&str] = &[
3541        "Undefined",
3542        "Null",
3543        "Boolean",
3544        "Number",
3545        "String",
3546        "Array",
3547        "Function",
3548        "Object",
3549        "Date",
3550        "RegExp",
3551        "Error",
3552    ];
3553    if NO_TAG.contains(&tag.as_str()) {
3554        return None;
3555    }
3556    Some(tag)
3557}
3558
3559/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3560/// carries an own `Symbol.toStringTag`, if any.
3561fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3562    let mut cur = h.proto_of(v);
3563    for _ in 0..100 {
3564        let p = cur?;
3565        if h.is_null(&p) {
3566            return None;
3567        }
3568        let name = match h.get(&p) {
3569            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3570            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3571        }
3572        // A CLASS prototype is not linked to the builtin its class extends —
3573        // the relationship lives on the class value — so the walk crosses over
3574        // there, or `Object.create(D.prototype)` for `class D extends Map`
3575        // finds nothing.
3576        .or_else(|| {
3577            h.class_owning_proto(&p)
3578                .and_then(|c| h.class_builtin_ancestor(&c))
3579                .map(|b| h.callable_name(&b))
3580                .filter(|n| !n.is_empty())
3581        });
3582        if let Some(n) = name {
3583            if intrinsic_proto_members(&format!("{n}.prototype"))
3584                .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3585            {
3586                return Some(n);
3587            }
3588        }
3589        cur = h.proto_of(&p);
3590    }
3591    None
3592}
3593
3594/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3595/// Every builtin exotic object reports its own brand, which is how packages
3596/// type-test values they did not construct (`toString.call(x) ===
3597/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3598/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3599pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3600    format!("[object {}]", object_brand(h, v))
3601}
3602
3603/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3604/// …), without the `[object …]` wrapper. Split out so the brand and the
3605/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3606/// Whether `v` is a function's `arguments` object.
3607///
3608/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3609/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3610/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3611pub fn is_arguments(v: &Value) -> bool {
3612    with_host(|h| is_arguments_h(h, v))
3613}
3614
3615/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3616/// runs under one, and re-entering through `with_host` aborts the process.
3617pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3618    h.fn_prop(v, "@@arguments").is_some()
3619}
3620
3621fn object_brand(h: &host::JsHost, v: &Value) -> String {
3622    // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3623    // it holds no instance slot, so only the branded few report anything but
3624    // `[object Object]`. Checked before the match because those prototypes are
3625    // plain `JsObj::Object`s and would otherwise be branded by whatever their
3626    // own properties happen to look like — `TypeError.prototype` has `name` and
3627    // `message`, which read as an Error instance.
3628    if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3629        return if BRANDED_PROTOS.contains(&ctor) {
3630            ctor.to_string()
3631        } else {
3632            "Object".to_string()
3633        };
3634    }
3635    let tag: String = match v {
3636        Value::Undef => "Undefined".into(),
3637        Value::Bool(_) => "Boolean".into(),
3638        Value::Int(_) | Value::Float(_) => "Number".into(),
3639        Value::Str(_) => "String".into(),
3640        Value::Obj(_) => match h.get(v) {
3641            Some(JsObj::Null) => "Null".into(),
3642            Some(JsObj::Str(_)) => "String".into(),
3643            Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3644            Some(JsObj::Array(_)) => "Array".into(),
3645            // A lazy iterator helper brands as node does.
3646            Some(JsObj::Object(p))
3647                if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3648            {
3649                "Iterator Helper".into()
3650            }
3651            // A `DOMException` brands by its class, not as a plain `Error`.
3652            Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3653            // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3654            // `Object.prototype.toString.call(new Number(1))` is
3655            // `[object Number]` rather than `[object Object]`.
3656            Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3657                Value::Bool(_) => "Boolean".into(),
3658                Value::Int(_) | Value::Float(_) => "Number".into(),
3659                _ => "String".into(),
3660            },
3661            // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3662            // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3663            // {}))` is `'[object Array]'`. Everything else about a proxy brands
3664            // as a plain Object (a `Symbol.toStringTag` read through the `get`
3665            // trap is handled by the caller, before this).
3666            Some(JsObj::Proxy { target, .. }) => {
3667                let mut cur = target;
3668                for _ in 0..100 {
3669                    match h.get(cur) {
3670                        Some(JsObj::Proxy { target: t, .. }) => cur = t,
3671                        _ => break,
3672                    }
3673                }
3674                match h.get(cur) {
3675                    Some(JsObj::Array(_)) => "Array".into(),
3676                    _ => "Object".into(),
3677                }
3678            }
3679            // `function*` / `async function` / `async function*` carry their own
3680            // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3681            Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3682                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3683                Some(d) if d.is_generator => "GeneratorFunction".into(),
3684                Some(d) if d.is_async => "AsyncFunction".into(),
3685                _ => "Function".into(),
3686            },
3687            // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3688            // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3689            Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3690                n.clone()
3691            }
3692            // A `<Ctor>.prototype` object brands as the constructor it belongs
3693            // to — `Object.prototype.toString.call(Set.prototype)` is
3694            // `[object Set]` — and a `require()`d module namespace is a plain
3695            // object. Neither is a function, so neither brands as one.
3696            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3697                match n.strip_suffix(".prototype") {
3698                    Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3699                    _ => "Object".into(),
3700                }
3701            }
3702            Some(JsObj::Class(_))
3703            | Some(JsObj::Builtin(_))
3704            | Some(JsObj::BoundFunc { .. })
3705            | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3706            // A suspended generator object is `[object Generator]`; an async one
3707            // `[object AsyncGenerator]`.
3708            Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3709            Some(JsObj::Generator { .. }) => "Generator".into(),
3710            Some(JsObj::RegExp(_)) => "RegExp".into(),
3711            Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3712            Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3713            Some(JsObj::Promise { .. }) => "Promise".into(),
3714            Some(JsObj::Symbol { .. }) => "Symbol".into(),
3715            Some(JsObj::BigInt(_)) => "BigInt".into(),
3716            // Native-tagged instances brand by their tag; a typed array brands by
3717            // its element kind (`@@kind`), and every Error subclass is `Error`.
3718            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3719                Some("TypedArray") => p
3720                    .get("@@kind")
3721                    .map(|k| h.str_of(k))
3722                    .unwrap_or_else(|| "Uint8Array".into()),
3723                Some("Buffer") => "Uint8Array".into(),
3724                // Every native class that really carries a `Symbol.toStringTag`
3725                // in Node brands by its own name. Verified against node v26:
3726                // `Object.prototype.toString.call(new WeakRef({}))` is
3727                // `[object WeakRef]`. The rest of the `@@native` tags
3728                // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3729                // classes with NO tag, so they stay `[object Object]` — listing
3730                // them here would invent a brand Node does not have.
3731                Some(
3732                    t @ ("ArrayBuffer"
3733                    | "DataView"
3734                    | "Date"
3735                    | "WeakRef"
3736                    | "FinalizationRegistry"
3737                    | "TextEncoder"
3738                    | "TextDecoder"
3739                    | "URL"
3740                    | "URLSearchParams"),
3741                ) => t.into(),
3742                _ if has_error_data(h, v) => "Error".into(),
3743                _ => "Object".into(),
3744            },
3745            _ => "Object".into(),
3746        },
3747        // node-js only produces the Value variants above; fusevm's shell-oriented
3748        // variants never arise here.
3749        _ => "Object".into(),
3750    };
3751    // Nothing about the value itself brands it. An ordinary object whose CHAIN
3752    // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3753    // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3754    // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3755    // `[object Map]` and was `[object Object]`.
3756    //
3757    // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3758    // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3759    // ACCESSOR returning the specific kind, so consulting the chain FIRST
3760    // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3761    // — three records caught it. `Error.prototype` carries no tag at all, so
3762    // inheriting from it borrows nothing.
3763    if tag == "Object" && !has_error_data(h, v) {
3764        if let Some(ctor) = chain_tag_ctor(h, v) {
3765            return ctor;
3766        }
3767    }
3768    tag
3769}
3770
3771fn b_setattr(vm: &mut VM, _: u8) -> Value {
3772    let val = vm.pop();
3773    let name = sval(&vm.pop());
3774    let recv = vm.pop();
3775    if let Err(e) = set_property(&recv, &name, val.clone()) {
3776        return abort(vm, e);
3777    }
3778    val
3779}
3780
3781/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3782/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3783/// `class C { static [k] = function(){} }`.
3784///
3785/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3786/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3787/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3788/// unconditionally here.
3789///
3790/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3791/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3792fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3793    let func = vm.pop();
3794    let kind = vm.pop().to_int();
3795    let key = vm.pop();
3796    let key = sval(&key);
3797    // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3798    // description gives the empty name, not `[undefined]`.
3799    let base = match with_host(|h| h.symbol_of_key(&key)) {
3800        Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3801            Some(JsObj::Symbol {
3802                desc: Some(desc), ..
3803            }) => format!("[{desc}]"),
3804            _ => String::new(),
3805        },
3806        None => key,
3807    };
3808    let name = match kind {
3809        host::member::GET => format!("get {base}"),
3810        host::member::SET => format!("set {base}"),
3811        _ => base,
3812    };
3813    with_host(|h| {
3814        let s = h.new_str(name);
3815        h.set_fn_prop(&func, "name", s);
3816    });
3817    func
3818}
3819
3820/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3821/// on the same path a plain `o.k = v` takes.
3822pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3823    set_property(recv, name, val)
3824}
3825
3826/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3827/// or `None` when it has none. Reads through a Proxy's
3828/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3829pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3830    let k = with_host(|h| h.new_str(key.to_string()));
3831    let d = own_descriptor_pub(obj, k).ok()?;
3832    if matches!(d, Value::Undef) {
3833        return None;
3834    }
3835    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3836    // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3837    // the host itself, so reading inside the closure double-borrows.
3838    let value = field("value");
3839    let writable = field("writable");
3840    let configurable = field("configurable");
3841    let truthy = |v: &Value| with_host(|h| h.truthy(v));
3842    let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3843    Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3844}
3845
3846/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3847/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3848/// proxy `set` trap forwarding to it both need.
3849///
3850/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3851/// with `receiver` as `this`; a data property does not write to `target` at all
3852/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3853/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3854/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3855/// documented way to forward a write, so the recursion hit every faithful
3856/// handler.
3857pub fn set_with_receiver(
3858    target: &Value,
3859    key: &str,
3860    val: Value,
3861    receiver: &Value,
3862) -> Result<bool, String> {
3863    // A proxy target answers through its own trap, which re-enters here with
3864    // whatever receiver the handler passes on.
3865    if crate::proxy::parts(target).is_some() {
3866        return crate::proxy::set(target, key, &val, receiver);
3867    }
3868    // An accessor anywhere on the target's chain wins, and sees `receiver`.
3869    if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3870        return match setter {
3871            Some(s) => {
3872                host::invoke(&s, vec![val], Some(receiver.clone()))?;
3873                Ok(true)
3874            }
3875            // A getter with no setter refuses the write rather than shadowing it.
3876            None => Ok(false),
3877        };
3878    }
3879    if !with_host(|h| h.can_write_prop(target, key)) {
3880        return Ok(false);
3881    }
3882    // Steps 3.b-3.d: only an object can receive the property, and its OWN
3883    // property decides — an accessor or a read-only slot refuses, and every
3884    // other case defines a plain data property.
3885    //
3886    // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3887    // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3888    // passed them through to `defineProperty`, which then threw `called on
3889    // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3890    if !with_host(|h| is_object_like(h, receiver)) {
3891        return Ok(false);
3892    }
3893    if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3894        if is_accessor || !writable {
3895            return Ok(false);
3896        }
3897    }
3898    // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3899    // by the receiver must not run, and a proxy receiver must reach its
3900    // `defineProperty` trap rather than its `set` trap.
3901    let desc = with_host(|h| {
3902        let mut m: IndexMap<String, Value> = IndexMap::new();
3903        m.insert("value".into(), val);
3904        m.insert("writable".into(), Value::Bool(true));
3905        m.insert("enumerable".into(), Value::Bool(true));
3906        m.insert("configurable".into(), Value::Bool(true));
3907        h.new_object(m)
3908    });
3909    if crate::proxy::parts(receiver).is_some() {
3910        return crate::proxy::define_property(receiver, key, &desc);
3911    }
3912    let k = with_host(|h| h.new_str(key.to_string()));
3913    define_property_pub(receiver, k, desc)?;
3914    Ok(true)
3915}
3916
3917/// Whether the first argument is a PRIMITIVE — including the three that ride as
3918/// heap handles, which a shape test misses.
3919fn is_primitive_arg(args: &[Value]) -> bool {
3920    let v = arg0(args);
3921    with_host(|h| host::is_primitive(h, &v))
3922}
3923
3924/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3925///
3926/// Adding a key to a non-extensible object reports differently from assigning
3927/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3928/// plain object, `[object Array]` for an array.
3929fn write_refused(recv: &Value, name: &str) -> String {
3930    let extensible = with_host(|h| h.is_extensible(recv));
3931    // Which of the two messages applies turns on whether the key already
3932    // EXISTS. Every shape that keeps its own properties in the fn-prop side
3933    // table answered a blanket `true` here, so adding a key to a frozen
3934    // function reported "read only" where node reports "not extensible".
3935    let has_own = with_host(|h| match h.get(recv) {
3936        Some(JsObj::Object(p)) => p.contains_key(name),
3937        Some(JsObj::Array(items)) => {
3938            name.parse::<usize>()
3939                .map(|i| i < items.len())
3940                .unwrap_or(false)
3941                || h.fn_prop(recv, name).is_some()
3942        }
3943        Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3944        _ => h.fn_prop(recv, name).is_some(),
3945    });
3946    if !extensible && !has_own {
3947        return host::type_error(&format!(
3948            "Cannot add property {name}, object is not extensible"
3949        ));
3950    }
3951    // The receiver renders the way every other brand-check message renders one
3952    // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3953    // class instance, `Error: m` for an error. Only Array was special-cased, so
3954    // every other exotic reported `#<Object>`.
3955    host::type_error(&format!(
3956        "Cannot assign to read only property '{name}' of object '{}'",
3957        no_side_effects_string(recv)
3958    ))
3959}
3960
3961fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3962    // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3963    // property of `undefined` or `null` throws, naming the key. Every such
3964    // write was silently discarded, so `u.x = 1` — the mirror of the single
3965    // most common runtime fault in JS, which the READ side already reports —
3966    // looked like it had succeeded.
3967    if with_host(|h| h.is_nullish(recv)) {
3968        return Err(host::type_error(&format!(
3969            "Cannot set properties of {} (setting '{name}')",
3970            with_host(|h| h.str_of(recv))
3971        )));
3972    }
3973    // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
3974    // throwaway wrapper — so it is discarded in sloppy code and throws in
3975    // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
3976    // `is_primitive` rather than a shape test: a string, a symbol and a bigint
3977    // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
3978    // value caught only numbers and booleans.
3979    if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
3980        return Err(host::type_error(&format!(
3981            "Cannot create property '{name}' on {} '{}'",
3982            with_host(|h| h.type_of(recv)),
3983            with_host(|h| h.str_of(recv))
3984        )));
3985    }
3986    // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
3987    // element. The class's own field initializers install theirs directly
3988    // (`host::init_one_field`), so a declaration never reaches this check.
3989    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
3990        return Err(private_brand_message(name, true));
3991    }
3992    // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
3993    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
3994        // A `set` trap that returns falsish REFUSED the write: silent in sloppy
3995        // code, a TypeError in strict, exactly as an ordinary refused write is.
3996        if crate::proxy::set(recv, name, &val, recv)? {
3997            return Ok(());
3998        }
3999        if with_host(|h| h.current_strict()) {
4000            return Err(host::type_error(&format!(
4001                "'set' on proxy: trap returned falsish for property '{name}'"
4002            )));
4003        }
4004        return Ok(());
4005    }
4006    // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4007    // back. Writing only the own property left the two views disagreeing:
4008    // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4009    if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4010        with_host(|h| h.set_name(name, val.clone()));
4011    }
4012    // `obj.__proto__ = p` re-links the prototype — but only for the two values
4013    // the Annex B setter accepts, an Object or `null`. Everything else is a
4014    // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4015    // untouched and creates no own key), and a null-prototype object inherits
4016    // no such setter at all, so there the assignment is an ORDINARY own
4017    // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4018    // prototype to the number 5.
4019    if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4020        if with_host(|h| h.has_null_proto(recv)) {
4021            // falls through to the ordinary own-property write below
4022        } else {
4023            let assignable =
4024                with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4025            if assignable {
4026                // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4027                // NON-EXTENSIBLE object refuses — and unlike an ordinary
4028                // refused write, the setter throws in sloppy code too. It was
4029                // rewriting the link of a frozen object.
4030                if would_cycle(recv, &val) {
4031                    return Err(host::type_error("Cyclic __proto__ value"));
4032                }
4033                if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4034                    return Err(host::type_error(&format!(
4035                        "{} is not extensible",
4036                        no_side_effects_string(recv)
4037                    )));
4038                }
4039                with_host(|h| h.set_proto(recv, val));
4040            }
4041            return Ok(());
4042        }
4043    }
4044    // Every environment value is a STRING. `process.env.PORT = 8080` stores
4045    // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4046    // process; storing the number made it add instead.
4047    if !name.starts_with("@@")
4048        && with_host(
4049            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4050        )
4051    {
4052        let text = with_host(|h| h.str_of(&val));
4053        // Write THROUGH to the real environment as well. `process.env` is not a
4054        // private map: node applies the change to the process, so a child
4055        // spawned afterwards inherits it. Keeping it only in the JS object meant
4056        // `process.env.NODE_ENV = 'production'` was invisible to every
4057        // `spawnSync`/`execSync` that followed.
4058        std::env::set_var(name, &text);
4059        let sv = with_host(|h| h.new_str(text));
4060        with_host(|h| {
4061            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4062                p.insert(name.to_string(), sv);
4063            }
4064        });
4065        return Ok(());
4066    }
4067    // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4068    // no later read re-derives a header over the top of the assigned value.
4069    if name == "stack" {
4070        with_host(|h| {
4071            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4072                p.shift_remove("@@stackRaw");
4073            }
4074        });
4075    }
4076    // An inherited/own setter accessor intercepts the write. This is checked
4077    // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4078    // kind first: `writable` is a data-property attribute and means nothing on
4079    // an accessor, where the setter alone decides. Testing it first meant an
4080    // accessor defined through `Object.defineProperty` — which leaves
4081    // `writable` false, having no such field — silently swallowed every write
4082    // instead of calling its setter, so the standard clone idiom
4083    // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4084    // object whose setters did nothing. An accessor from an object literal
4085    // carries all-true attributes, which is why only the former broke.
4086    if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4087        if let Some(setter) = setter {
4088            let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4089            return Ok(());
4090        }
4091        // Only a getter: the write is refused — silent in sloppy mode, a
4092        // TypeError in strict code. The `return` above matters, since a
4093        // successful setter call must not fall into this.
4094        let _ = getter;
4095        if with_host(|h| h.current_strict()) {
4096            return Err(host::type_error(&format!(
4097                "Cannot set property {name} of #<Object> which has only a getter"
4098            )));
4099        }
4100        return Ok(());
4101    }
4102    // A non-writable property, or a new key on a non-extensible object, refuses
4103    // the write. In SLOPPY mode that is silent; in strict code it is a
4104    // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4105    // refusal used to be silent, so `'use strict'` did not catch a write to a
4106    // frozen object, which is most of the reason to freeze one.
4107    if !with_host(|h| h.can_write_prop(recv, name)) {
4108        if with_host(|h| h.current_strict()) {
4109            return Err(write_refused(recv, name));
4110        }
4111        return Ok(());
4112    }
4113    // Writing `name`/`prototype`/statics on a function value.
4114    if matches!(
4115        with_host(|h| h.kind_of(recv)),
4116        Some(ObjKind::Func) | Some(ObjKind::Class)
4117    ) {
4118        with_host(|h| h.set_fn_prop(recv, name, val));
4119        return Ok(());
4120    }
4121    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4122    // Each bare reference is a fresh `Builtin` handle, so route to the stable
4123    // per-namespace side table rather than the per-index `fn_props`.
4124    if let Some(ns) = peek(recv, |o| match o {
4125        JsObj::Builtin(ns) => Some(ns.clone()),
4126        _ => None,
4127    }) {
4128        // `process.exitCode` is an accessor in Node, not a data property: the
4129        // setter validates and stores the code the process will finally exit
4130        // with. Landing it in the generic static table made it a write-only
4131        // decoration — `process.exitCode = 3` read back as 3 and the process
4132        // still exited 0.
4133        if ns == "process" && name == "exitCode" {
4134            return crate::stdlib::process::set_exit_code(&val);
4135        }
4136        with_host(|h| h.set_builtin_static(&ns, name, val));
4137        return Ok(());
4138    }
4139    // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4140    // `String.prototype`, `TypeError.prototype`) is mirrored into the
4141    // per-namespace side table as well as the object's own map. Instances are
4142    // not linked to these objects by `proto_of` — the chain walk never reaches
4143    // them — so the mirror is what makes `String.prototype.pad = f` visible as
4144    // `"x".pad`. The own-map write below still happens, so reading the
4145    // prototype itself and enumerating it keep working unchanged.
4146    if let Some(ns) = with_host(|h| {
4147        h.intrinsic_proto_ctor(recv)
4148            .map(str::to_string)
4149            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4150    }) {
4151        with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4152    }
4153    // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4154    // writability check above already refused it on a FROZEN regexp, which it
4155    // could only do once `integrity_keys` learned that `lastIndex` is an own
4156    // property.
4157    if name == "lastIndex" {
4158        if let Some(n) = with_host(|h| match h.get(recv) {
4159            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4160            _ => None,
4161        }) {
4162            with_host(|h| {
4163                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4164                    r.last_index = if n.is_finite() && n >= 0.0 {
4165                        crate::utf16::U16Index::new(n as usize)
4166                    } else {
4167                        crate::utf16::U16Index::ZERO
4168                    };
4169                }
4170            });
4171            return Ok(());
4172        }
4173    }
4174    // An `arguments` object is an ORDINARY object with a `length` data property,
4175    // not an array: a write PAST the end adds an index and leaves `length`
4176    // alone. The array backing grew it instead, so `f(1)` followed by
4177    // `arguments[1] = 9` reported `arguments.length` as 2.
4178    if let Ok(i) = name.parse::<usize>() {
4179        if is_arguments(recv) && i >= array_len(recv) {
4180            with_host(|h| h.set_fn_prop(recv, name, val));
4181            return Ok(());
4182        }
4183    }
4184    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4185    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4186        let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4187        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4188            return Ok(());
4189        }
4190        // An index write to a view over a DETACHED buffer is DROPPED. Falling
4191        // through would store it as an ordinary own property, which then showed
4192        // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4193        if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4194            return Ok(());
4195        }
4196        // `buf[i] = n` writes through to the Buffer's hidden byte array.
4197        if crate::stdlib::buffer::byte_set(recv, name, &val) {
4198            return Ok(());
4199        }
4200    }
4201    // Any own property on an exotic with no property map of its own. This sits
4202    // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4203    // moves its match cursor rather than being shadowed by a side-table entry.
4204    if uses_side_table(recv) {
4205        with_host(|h| h.set_fn_prop(recv, name, val));
4206        return Ok(());
4207    }
4208    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4209    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4210        && name != "length"
4211        && name.parse::<usize>().is_err()
4212    {
4213        with_host(|h| h.set_fn_prop(recv, name, val));
4214        return Ok(());
4215    }
4216    // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4217    // and does so outside the host borrow because `ToNumber` may run a user
4218    // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4219    let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4220        let want = host::to_array_length(&val)?;
4221        // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4222        // STOPS at the first element that cannot be deleted, leaving the length
4223        // just past it. Truncating regardless discarded a non-configurable
4224        // element and reported a length node would not have accepted.
4225        let floor = with_host(|h| {
4226            let old = match h.get(recv) {
4227                Some(JsObj::Array(items)) => items.len(),
4228                _ => 0,
4229            };
4230            let mut stop = want;
4231            for i in (want..old).rev() {
4232                if !h.prop_attrs(recv, &i.to_string()).configurable {
4233                    stop = i + 1;
4234                    break;
4235                }
4236            }
4237            stop
4238        });
4239        Some(floor.max(want))
4240    } else {
4241        None
4242    };
4243    with_host(|h| match h.get_mut(recv) {
4244        Some(JsObj::Object(props)) => {
4245            // Adding a *new* array-index key must re-place it into ascending
4246            // integer-key order (updating an existing key keeps its position).
4247            let is_new = !props.contains_key(name);
4248            props.insert(name.to_string(), val);
4249            if is_new && host::array_index(name).is_some() {
4250                host::canonicalize_own_keys(props);
4251            }
4252        }
4253        Some(JsObj::Array(items)) => {
4254            if let Some(n) = new_len {
4255                // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4256                // just the one own key); shrinking drops any hole past the end.
4257                let old = items.len();
4258                items.resize(n, Value::Undef);
4259                if n > old {
4260                    h.mark_hole_range(recv, old..n);
4261                } else {
4262                    h.truncate_holes(recv, n);
4263                }
4264            } else if let Ok(i) = name.parse::<usize>() {
4265                // A write PAST the end leaves the skipped positions elided.
4266                let old = items.len();
4267                if i >= old {
4268                    items.resize(i + 1, Value::Undef);
4269                }
4270                items[i] = val;
4271                if i > old {
4272                    h.mark_hole_range(recv, old..i);
4273                }
4274                // …and the written index itself is no longer one. This is the
4275                // single site that keeps a hole record from outliving the
4276                // elision it describes: every array element write in the
4277                // language reaches it.
4278                h.clear_hole(recv, i);
4279            }
4280        }
4281        _ => {}
4282    });
4283    Ok(())
4284}
4285
4286fn b_getitem(vm: &mut VM, _: u8) -> Value {
4287    let idx = vm.pop();
4288    let recv = vm.pop();
4289    let key = match host::to_property_key(&idx) {
4290        Ok(k) => k,
4291        Err(e) => return abort(vm, e),
4292    };
4293    match get_property(&recv, &key) {
4294        Ok(v) => v,
4295        Err(e) => abort(vm, e),
4296    }
4297}
4298
4299fn b_setitem(vm: &mut VM, _: u8) -> Value {
4300    let val = vm.pop();
4301    let idx = vm.pop();
4302    let recv = vm.pop();
4303    let key = match host::to_property_key(&idx) {
4304        Ok(k) => k,
4305        Err(e) => return abort(vm, e),
4306    };
4307    if let Err(e) = set_property(&recv, &key, val.clone()) {
4308        return abort(vm, e);
4309    }
4310    val
4311}
4312
4313/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4314/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4315/// the three cannot drift. Reports `false` for a non-configurable property
4316/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4317/// which is also what deleting an absent key reports.
4318pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4319    // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4320    // `delete u.x` reported success instead.
4321    if with_host(|h| h.is_nullish(recv)) {
4322        return Err(host::type_error(
4323            "Cannot convert undefined or null to object",
4324        ));
4325    }
4326    // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4327    // throw — the reason this reports a `Result` rather than a bare `bool`.
4328    if let Some(b) = crate::proxy::delete(recv, key)? {
4329        return Ok(b);
4330    }
4331    // `delete globalThis.x` removes a global a script created. It lives in the
4332    // globals map, not the object's property map, so the ordinary path reported
4333    // success and removed nothing — the binding stayed readable afterwards.
4334    if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4335        return Ok(true);
4336    }
4337    // `delete require.cache[id]` drops the module so the next `require` of that
4338    // file runs it again — the whole point of exposing the cache.
4339    if peek(recv, |o| match o {
4340        JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4341        _ => None,
4342    }) == Some(true)
4343    {
4344        return Ok(crate::module::cache_delete(key));
4345    }
4346    // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4347    // JS view, so a child spawned afterwards no longer sees it.
4348    if !key.starts_with("@@")
4349        && with_host(
4350            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4351        )
4352    {
4353        std::env::remove_var(key);
4354    }
4355    // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4356    // `Object.prototype`) is non-configurable when it is a constant or a
4357    // constructor's `prototype`, and `delete` of one answers false without
4358    // removing anything. There is no property map behind a namespace, so the
4359    // ordinary attribute lookup below cannot tell — it reported success for
4360    // every one of them.
4361    // A REAL intrinsic prototype object carries the write in its own map AND in
4362    // the side table the instance read consults, so the delete has to clear
4363    // both. Clearing only the map left `Object.prototype.patch` deleted as far
4364    // as the prototype was concerned and still inherited by every object.
4365    if let Some(ns) = with_host(|h| {
4366        h.intrinsic_proto_ctor(recv)
4367            .map(str::to_string)
4368            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4369    }) {
4370        with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4371    }
4372    if let Some(ns) = peek(recv, |o| match o {
4373        JsObj::Builtin(ns) => Some(ns.clone()),
4374        _ => None,
4375    }) {
4376        // A script-assigned static is an ordinary configurable property and is
4377        // removed from the side table the assignment landed in. Falling through
4378        // to the attribute check below answered true and deleted nothing, so a
4379        // patch survived its own `delete`.
4380        if with_host(|h| h.remove_builtin_static(&ns, key)) {
4381            return Ok(true);
4382        }
4383        if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4384            return Ok(false);
4385        }
4386    }
4387    if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4388        return Ok(false);
4389    }
4390    // An accessor lives in its own table, not the property map, so removing it
4391    // has to be explicit — otherwise `delete` reported success while the getter
4392    // kept answering and `in` kept reporting the key.
4393    if with_host(|h| h.own_accessor(recv, key).is_some()) {
4394        with_host(|h| h.remove_accessor(recv, key));
4395        return Ok(true);
4396    }
4397    with_host(|h| {
4398        let index = key.parse::<usize>();
4399        match h.get_mut(recv) {
4400            Some(JsObj::Object(props)) => {
4401                props.shift_remove(key);
4402                return;
4403            }
4404            Some(JsObj::Array(items)) => {
4405                if let Ok(i) = index {
4406                    if i < items.len() {
4407                        // `delete a[i]` punches a HOLE: the length is unchanged
4408                        // but the index stops being an own property.
4409                        items[i] = Value::Undef;
4410                        h.mark_hole(recv, i);
4411                    }
4412                    return;
4413                }
4414            }
4415            _ => {}
4416        }
4417        // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4418        // a function/class, is an ordinary own property kept in the side table.
4419        h.remove_fn_prop(recv, key);
4420    });
4421    Ok(true)
4422}
4423
4424fn b_delitem(vm: &mut VM, _: u8) -> Value {
4425    let strict = vm.pop();
4426    let idx = vm.pop();
4427    let recv = vm.pop();
4428    // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4429    // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4430    // description and delete a key nothing ever wrote.
4431    let key = match host::to_property_key(&idx) {
4432        Ok(k) => k,
4433        Err(e) => return abort(vm, e),
4434    };
4435    match delete_property(&recv, &key) {
4436        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4437            abort(vm, refused_delete_error(&recv, &key))
4438        }
4439        Ok(b) => Value::Bool(b),
4440        Err(e) => abort(vm, e),
4441    }
4442}
4443
4444fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4445    let strict = vm.pop();
4446    let name = sval(&vm.pop());
4447    let recv = vm.pop();
4448    match delete_property(&recv, &name) {
4449        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4450            abort(vm, refused_delete_error(&recv, &name))
4451        }
4452        Ok(b) => Value::Bool(b),
4453        Err(e) => abort(vm, e),
4454    }
4455}
4456
4457/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4458/// receiver renders the way every other brand-check message renders one.
4459fn refused_delete_error(recv: &Value, key: &str) -> String {
4460    // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4461    // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4462    // here rather than in the shared `[[Delete]]`.
4463    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4464        return host::type_error(&format!(
4465            "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4466        ));
4467    }
4468    // A non-callable builtin NAMESPACE renders as a plain object here — node
4469    // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4470    let shown = match peek(recv, |o| match o {
4471        JsObj::Builtin(ns) => Some(ns.clone()),
4472        _ => None,
4473    }) {
4474        Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4475        _ => no_side_effects_string(recv),
4476    };
4477    host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4478}
4479
4480// ── constructors ──────────────────────────────────────────────────────────────
4481
4482fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4483    let parts = pop_n(vm, argc as usize);
4484    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4485    with_host(|h| h.new_str(s))
4486}
4487
4488fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4489    let items = pop_n(vm, argc as usize);
4490    with_host(|h| h.new_array(items))
4491}
4492
4493/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4494/// only for an array literal that actually contains an elision, so a dense
4495/// literal costs nothing. Returns `undefined`; the array stays on the stack
4496/// underneath (the compiler `Dup`s it).
4497fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4498    let idx = vm.pop();
4499    let arr = vm.pop();
4500    let i = match idx {
4501        Value::Int(i) if i >= 0 => i as usize,
4502        _ => return Value::Undef,
4503    };
4504    with_host(|h| h.mark_hole(&arr, i));
4505    Value::Undef
4506}
4507
4508fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4509    let flat = pop_n(vm, argc as usize);
4510    let mut props: IndexMap<String, Value> = IndexMap::new();
4511    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4512    let mut proto_override: Option<Value> = None;
4513    let mut method_keys: Vec<String> = Vec::new();
4514    let mut i = 0;
4515    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4516        if i + 2 >= flat.len() {
4517            break;
4518        }
4519        // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4520        // the literal reserves its slot here with the `@@ord:` marker key that
4521        // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4522        // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4523        // marker landed at the end.
4524        if matches!(flat[i], Value::Int(2)) {
4525            let key = with_host(|h| h.str_of(&flat[i + 1]));
4526            props
4527                .entry(format!("{}{key}", host::ORD_MARKER))
4528                .or_insert(Value::Undef);
4529            i += 3;
4530            continue;
4531        }
4532        // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4533        // recorded so the literal can become its `[[HomeObject]]` below.
4534        if matches!(flat[i], Value::Int(3)) {
4535            let key = with_host(|h| h.str_of(&flat[i + 1]));
4536            method_keys.push(key.clone());
4537            props.insert(key, flat[i + 2].clone());
4538            i += 3;
4539            continue;
4540        }
4541        let spread = matches!(flat[i], Value::Int(1));
4542        if spread {
4543            let src = flat[i + 1].clone();
4544            // A STRING source spreads its index properties (`{..."ab"}` is
4545            // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4546            // String exotic object owns one enumerable property per UTF-16 code
4547            // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4548            // a string source contributed nothing and `{..."ab"}` was `{}`.
4549            // Every other primitive (number/boolean/symbol) boxes to an object
4550            // with no own enumerable properties, and null/undefined are ignored,
4551            // so those correctly stay no-ops on the path below.
4552            if let Some(s) = with_host(|h| h.as_str(&src)) {
4553                for idx in 0..crate::utf16::len(&s) {
4554                    if let Ok(ch) = get_property(&src, &idx.to_string()) {
4555                        props.insert(idx.to_string(), ch);
4556                    }
4557                }
4558                i += 3;
4559                continue;
4560            }
4561            // Object spread copies own *enumerable* properties only — never the
4562            // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4563            // into something that still claimed to be a Buffer) and never a
4564            // property a descriptor marked non-enumerable.
4565            // A getter that throws during spread propagates as a thrown value,
4566            // which in the VM means aborting the frame.
4567            let entries = match host::own_enum_entries_deep(&src) {
4568                Ok(e) => e,
4569                Err(e) => return abort(vm, e),
4570            };
4571            for (k, v) in entries {
4572                props.insert(k, v);
4573            }
4574            // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4575            // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4576            for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4577                props.insert(k, v);
4578            }
4579        } else {
4580            let key = with_host(|h| h.str_of(&flat[i + 1]));
4581            if key == "__proto__" {
4582                proto_override = Some(flat[i + 2].clone());
4583            } else {
4584                props.insert(key, flat[i + 2].clone());
4585            }
4586        }
4587        i += 3;
4588    }
4589    with_host(|h| {
4590        let o = h.new_object(props);
4591        if let Some(p) = proto_override {
4592            if matches!(p, Value::Obj(_)) {
4593                h.set_proto(&o, p);
4594            }
4595        }
4596        // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4597        // is what `super` inside it resolves through. The home object is fixed
4598        // at definition, so a method that merely arrives as a value
4599        // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4600        // method-valued property instead rebound the original and changed what
4601        // IT resolved.
4602        for key in &method_keys {
4603            let m = match h.get(&o) {
4604                Some(JsObj::Object(p)) => p.get(key).cloned(),
4605                _ => None,
4606            };
4607            if let Some(m) = m {
4608                if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4609                    f.home_object = Some(o.clone());
4610                }
4611            }
4612        }
4613        o
4614    })
4615}
4616
4617fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4618    let def_id = match vm.pop() {
4619        Value::Int(n) => n as usize,
4620        Value::Float(f) => f as usize,
4621        _ => return abort(vm, "internal: MKFUNC id".into()),
4622    };
4623    let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4624        Some(d) => (
4625            d.is_arrow,
4626            (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4627        ),
4628        None => (false, None),
4629    });
4630    with_host(|h| {
4631        let mut env = h.current_env_capture();
4632        let this = h.current_this();
4633        // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4634        // exactly as it uses the enclosing `this`. Nothing was captured, so
4635        // `super.m()` inside an arrow reported the method missing — in a class
4636        // method as well as an object literal.
4637        let (home_class, home_static, home_object) = if is_arrow {
4638            h.current_home()
4639        } else {
4640            (None, false, None)
4641        };
4642        // A named function expression closes over an extra scope holding its own
4643        // name, so the body can recurse through it (`function f(){ … f() … }`)
4644        // independently of whatever the outer binding is later set to.
4645        if self_name.is_some() {
4646            env = host::child_env(env);
4647        }
4648        let f = h.alloc(JsObj::Func(FuncVal {
4649            def_id,
4650            env: Some(env.clone()),
4651            this,
4652            is_arrow,
4653            home_class,
4654            home_static,
4655            home_object,
4656        }));
4657        if let Some(n) = self_name {
4658            env.borrow_mut().vars.insert(n, f.clone());
4659        }
4660        f
4661    })
4662}
4663
4664// ── truthiness / coercion / equality ──────────────────────────────────────────
4665
4666fn b_truthy(vm: &mut VM, _: u8) -> Value {
4667    let v = vm.pop();
4668    Value::Bool(with_host(|h| h.truthy(&v)))
4669}
4670
4671fn b_nullish(vm: &mut VM, _: u8) -> Value {
4672    let v = vm.pop();
4673    Value::Bool(with_host(|h| h.is_nullish(&v)))
4674}
4675
4676fn b_tostr(vm: &mut VM, _: u8) -> Value {
4677    let v = vm.pop();
4678    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4679    // `String(x)`, object keys).
4680    match host::to_string_value(&v) {
4681        Ok(s) => s,
4682        Err(e) => abort(vm, e),
4683    }
4684}
4685
4686fn b_typeof(vm: &mut VM, _: u8) -> Value {
4687    let v = vm.pop();
4688    with_host(|h| {
4689        let t = h.type_of(&v);
4690        h.new_str(t)
4691    })
4692}
4693
4694/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4695/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4696fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4697    let name = sval(&vm.pop());
4698    // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4699    // for an UNBOUND name, but a `let` above its declaration is bound and
4700    // throws. Reading the marker's type answered "function".
4701    if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4702        return abort(vm, host::tdz_error(&name));
4703    }
4704    if let Some(v) = with_host(|h| h.read_name(&name)) {
4705        if with_host(|h| h.is_tdz(&v)) {
4706            return abort(vm, host::tdz_error(&name));
4707        }
4708    }
4709    // Bound name (user variable) → typeof its value.
4710    if let Some(v) = with_host(|h| h.read_name(&name)) {
4711        return with_host(|h| {
4712            let t = h.type_of(&v);
4713            h.new_str(t)
4714        });
4715    }
4716    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4717    // would produce, then take its type (so object-namespaces like `console`/
4718    // `Math`/`JSON`/`process` report "object", constructors report "function").
4719    let t = match name.as_str() {
4720        "undefined" => "undefined".to_string(),
4721        "NaN" | "Infinity" => "number".to_string(),
4722        "globalThis" | "global" => "object".to_string(),
4723        n if is_namespace(n) || is_known_builtin(n) => {
4724            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4725            with_host(|h| h.type_of(&v)).to_string()
4726        }
4727        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4728    };
4729    with_host(|h| h.new_str(t))
4730}
4731
4732fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4733    let b = vm.pop();
4734    let a = vm.pop();
4735    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4736}
4737
4738fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4739    let b = vm.pop();
4740    let a = vm.pop();
4741    // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4742    // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4743    // runs before the host borrow. Object ⇄ object stays a reference check.
4744    let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4745        (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4746            Ok(p) => (p, b),
4747            Err(e) => return abort(vm, e),
4748        },
4749        (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4750            Ok(p) => (a, p),
4751            Err(e) => return abort(vm, e),
4752        },
4753        _ => (a, b),
4754    };
4755    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4756}
4757
4758fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4759    let ctor = vm.pop();
4760    let obj = vm.pop();
4761    match host::instance_of(&obj, &ctor) {
4762        Ok(b) => Value::Bool(b),
4763        Err(e) => abort(vm, e),
4764    }
4765}
4766
4767// ── bitwise / unary ───────────────────────────────────────────────────────────
4768
4769fn b_binop(vm: &mut VM, _: u8) -> Value {
4770    let b = vm.pop();
4771    let a = vm.pop();
4772    let tag = match vm.pop() {
4773        Value::Int(n) => n,
4774        _ => 0,
4775    };
4776    // Both operands are ToPrimitive-d with the number hint before ToInt32
4777    // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4778    let r = host::to_primitive(&a, "number")
4779        .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4780        .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4781    finish(vm, r)
4782}
4783
4784fn b_unary(vm: &mut VM, _: u8) -> Value {
4785    let v = vm.pop();
4786    let tag = match vm.pop() {
4787        Value::Int(n) => n,
4788        _ => 0,
4789    };
4790    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4791    // computed in arbitrary precision.
4792    if with_host(|h| h.is_bigint_val(&v)) {
4793        return match tag {
4794            host::unop::POS => abort(
4795                vm,
4796                host::type_error("Cannot convert a BigInt value to a number"),
4797            ),
4798            host::unop::BITNOT => {
4799                let b = with_host(|h| h.as_bigint(&v)).unwrap();
4800                let r = -(b + num_bigint::BigInt::from(1));
4801                with_host(|h| h.new_bigint(r))
4802            }
4803            _ => Value::Undef,
4804        };
4805    }
4806    // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4807    // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4808    let n = match host::to_number_value(&v) {
4809        Ok(n) => n,
4810        Err(e) => return abort(vm, e),
4811    };
4812    match tag {
4813        host::unop::POS => Value::Float(n),
4814        host::unop::BITNOT => {
4815            let i = if n.is_finite() {
4816                n.trunc() as i64 as i32
4817            } else {
4818                0
4819            };
4820            Value::Float(!i as f64)
4821        }
4822        _ => Value::Undef,
4823    }
4824}
4825
4826// ── membership ────────────────────────────────────────────────────────────────
4827
4828fn b_contains(vm: &mut VM, _: u8) -> Value {
4829    let container = vm.pop();
4830    let key = vm.pop();
4831    // `x in y` requires y to be an object. V8 names both operands:
4832    // `Cannot use 'in' operator to search for 'a' in 5`.
4833    // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4834    // `Value::Obj` in this host but is not an object, so the shape test alone
4835    // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4836    // where node throws. `is_primitive` is the same predicate `ToObject` and
4837    // `typeof` use, so the three cannot disagree about what an object is.
4838    if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4839        let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4840        return abort(
4841            vm,
4842            host::type_error(&format!(
4843                "Cannot use 'in' operator to search for '{k}' in {c}"
4844            )),
4845        );
4846    }
4847    let k = match host::to_property_key(&key) {
4848        Ok(k) => k,
4849        Err(e) => return abort(vm, e),
4850    };
4851    match has_property(&container, &k) {
4852        Ok(b) => Value::Bool(b),
4853        Err(e) => abort(vm, e),
4854    }
4855}
4856
4857// ── control ───────────────────────────────────────────────────────────────────
4858
4859fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4860    let v = vm.pop();
4861    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4862    vm.ip = vm.chunk.ops.len();
4863    v
4864}
4865
4866/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4867/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4868/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4869fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4870    let label = sval(&vm.pop());
4871    let label = (!label.is_empty()).then_some(label);
4872    with_host(|h| h.signal = Some(host::Signal::Break(label)));
4873    vm.ip = vm.chunk.ops.len();
4874    Value::Undef
4875}
4876
4877/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4878fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4879    let label = sval(&vm.pop());
4880    let label = (!label.is_empty()).then_some(label);
4881    with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4882    vm.ip = vm.chunk.ops.len();
4883    Value::Undef
4884}
4885
4886/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4887/// describes what the `try` is nested in (see [`host::unwind`]):
4888///
4889/// * no signal → `NONE`, execution continues normally;
4890/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4891///   keeps travelling outward;
4892/// * `break`/`continue` targeting the enclosing loop → consume it and report
4893///   `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4894///   continue target;
4895/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4896///   the signal pending, so leaving this loop re-dispatches it one level out.
4897fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4898    let cont_tag = sval(&vm.pop());
4899    let brk_tag = sval(&vm.pop());
4900    let sig = match with_host(|h| h.signal.clone()) {
4901        Some(s) => s,
4902        None => return Value::Int(host::unwind::NONE),
4903    };
4904    // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4905    let propagate = |vm: &mut VM| {
4906        vm.ip = vm.chunk.ops.len();
4907        Value::Int(host::unwind::NONE)
4908    };
4909    match &sig {
4910        host::Signal::Return(_) => propagate(vm),
4911        host::Signal::Break(label) => {
4912            if brk_tag == host::unwind::NO_LOOP {
4913                return propagate(vm);
4914            }
4915            let mine = match label {
4916                None => true, // unlabeled: always the innermost enclosing context
4917                Some(l) => brk_tag == *l,
4918            };
4919            if mine {
4920                with_host(|h| h.signal = None);
4921            }
4922            // Not ours: still leave this context by its break exit, keeping the
4923            // signal pending for the next dispatch point one level out.
4924            Value::Int(host::unwind::BREAK)
4925        }
4926        host::Signal::Continue(label) => {
4927            let mine = match label {
4928                // Unlabeled `continue` binds to the innermost continue-catching
4929                // loop — which a `switch` between here and it is NOT.
4930                None => cont_tag != host::unwind::NO_LOOP,
4931                Some(l) => cont_tag == *l,
4932            };
4933            if mine {
4934                with_host(|h| h.signal = None);
4935                return Value::Int(host::unwind::CONTINUE);
4936            }
4937            if brk_tag == host::unwind::NO_LOOP {
4938                return propagate(vm);
4939            }
4940            // The target loop is further out: exit the innermost context here and
4941            // re-dispatch there.
4942            Value::Int(host::unwind::BREAK)
4943        }
4944    }
4945}
4946
4947fn b_throw(vm: &mut VM, _: u8) -> Value {
4948    let v = vm.pop();
4949    let msg = with_host(|h| {
4950        h.exc = Some(v.clone());
4951        // Prefer an error object's message for the top-level report.
4952        error_display(h, &v)
4953    });
4954    abort(vm, msg)
4955}
4956
4957fn error_display(h: &host::JsHost, v: &Value) -> String {
4958    if let Some(JsObj::Object(props)) = h.get(v) {
4959        let name = props
4960            .get("name")
4961            .map(|x| h.str_of(x))
4962            .unwrap_or_else(|| "Error".into());
4963        if let Some(m) = props.get("message") {
4964            return format!("Uncaught {name}: {}", h.str_of(m));
4965        }
4966    }
4967    format!("Uncaught {}", h.str_of(v))
4968}
4969
4970fn b_try(vm: &mut VM, _: u8) -> Value {
4971    let id = match vm.pop() {
4972        Value::Int(n) => n as usize,
4973        _ => return abort(vm, "internal: TRY id".into()),
4974    };
4975    // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
4976    // its handler and its finalizer bytecode — every time control entered it,
4977    // which for a `try` inside a loop is once per iteration.
4978    let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
4979        Some(t) => t,
4980        None => return abort(vm, "internal: unknown try id".into()),
4981    };
4982    let mut pending: Option<String> = None;
4983    // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
4984    // through can leave block scopes open. Snapshot the scope and restore it
4985    // before the handler and after the whole statement.
4986    let scope = with_host(|h| h.scope_snapshot());
4987
4988    with_host(|h| h.push_scope()); // the try block is its own block scope
4989    let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
4990        with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
4991    });
4992    with_host(|h| h.restore_scope(scope.clone()));
4993    let signal_after = with_host(|h| h.signal.is_some());
4994    if let Err(e) = body_res {
4995        if signal_after {
4996            pending = Some(e);
4997        } else if has_handler {
4998            // Bind the thrown value (or a synthesized error) to the catch param.
4999            let thrown =
5000                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5001            with_host(|h| {
5002                h.error = None;
5003                h.exc = None;
5004            });
5005            // The catch parameter is block-scoped to the handler.
5006            with_host(|h| h.push_scope());
5007            if let Some(name) = &catch_bind {
5008                with_host(|h| h.declare_name(name, thrown));
5009            }
5010            let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5011                with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5012            });
5013            with_host(|h| h.restore_scope(scope.clone()));
5014            if let Err(e2) = hres {
5015                pending = Some(e2);
5016            }
5017        } else {
5018            pending = Some(e);
5019        }
5020    }
5021
5022    // finally always runs; a finally error/signal supersedes.
5023    if has_finalizer {
5024        let sig_before = with_host(|h| h.signal.take());
5025        with_host(|h| h.push_scope()); // ditto for `finally`
5026        let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5027            with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5028        });
5029        with_host(|h| h.restore_scope(scope.clone()));
5030        match fres {
5031            Ok(_) => {
5032                if with_host(|h| h.signal.is_none()) {
5033                    // The finalizer completed normally: the try/catch block's own
5034                    // abrupt completion resumes.
5035                    with_host(|h| h.signal = sig_before);
5036                } else {
5037                    // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5038                    // completion is abrupt (`return`/`break`/`continue` inside
5039                    // `finally`), that completion REPLACES the try/catch block's —
5040                    // including a pending throw, which is discarded, not rethrown.
5041                    pending = None;
5042                    with_host(|h| {
5043                        h.error = None;
5044                        h.exc = None;
5045                    });
5046                }
5047            }
5048            Err(e) => pending = Some(e),
5049        }
5050    }
5051
5052    if let Some(e) = pending {
5053        return abort(vm, e);
5054    }
5055    Value::Undef
5056}
5057
5058/// Synthesize an `Error`-shaped object from an internal error string, linked to
5059/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5060pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5061    h.ensure_error_protos();
5062    // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5063    // it is not one of the ECMAScript error classes below.
5064    if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5065        if let Some((name, msg)) = rest.split_once('\u{1}') {
5066            return dom_exception_with(h, name, msg);
5067        }
5068    }
5069    // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5070    // error class, exactly as Node's internal errors render it in `.stack`.
5071    let (head, rest) = match e.split_once(": ") {
5072        Some((n, m)) => (n, m.to_string()),
5073        None => ("", e.to_string()),
5074    };
5075    let (base, code) = match head.split_once(" [") {
5076        Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5077        _ => (head, None),
5078    };
5079    let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5080        (base.to_string(), rest)
5081    } else {
5082        ("Error".to_string(), e.to_string())
5083    };
5084    // A `host::plain_coded_error` marker: the code rides at the head of the
5085    // MESSAGE rather than in the class, because Node's native-layer errors set
5086    // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5087    // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5088    // internal and must never reach a user-visible `.message`.
5089    let mut code = code;
5090    // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5091    // bracketed head, false for the marker form.
5092    let mut bracketed = code.is_some();
5093    // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5094    let mut fields: Vec<(String, String)> = Vec::new();
5095    if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5096        if let Some((c, m)) = rest.split_once('\u{1}') {
5097            code = Some(c.to_string());
5098            bracketed = false;
5099            let (m, fs) = host::split_error_fields(m);
5100            fields = fs
5101                .into_iter()
5102                .map(|(k, v)| (k.to_string(), v.to_string()))
5103                .collect();
5104            message = m.to_string();
5105        }
5106    }
5107    let mut props: IndexMap<String, Value> = IndexMap::new();
5108    let mv = h.new_str(message.clone());
5109    props.insert("message".into(), mv);
5110    if let Some(c) = &code {
5111        let cv = h.new_str(c.clone());
5112        props.insert("code".into(), cv);
5113        for (k, v) in fields {
5114            let fv = h.new_str(v);
5115            props.insert(k, fv);
5116        }
5117        if bracketed {
5118            // Marks this as a Node JS-layer error, whose `toString` brackets the
5119            // code. A native-layer error has the same `.code` and does not.
5120            props.insert("@@nodeError".into(), Value::Bool(true));
5121        }
5122    }
5123    let label = match (&code, bracketed) {
5124        (Some(c), true) => format!("{name} [{c}]"),
5125        _ => name.clone(),
5126    };
5127    let frames = h.stack_frames();
5128    let stack = if message.is_empty() {
5129        format!("{label}{frames}")
5130    } else {
5131        format!("{label}: {message}{frames}")
5132    };
5133    let sv = h.new_str(stack);
5134    props.insert("stack".into(), sv);
5135    // A libuv system-error message is itself the canonical encoding of the
5136    // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5137    // filesystem/network failure recovers the enumerable `code`/`errno`/
5138    // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5139    // single most common error-handling idiom in Node packages) depend on.
5140    for (k, v) in syscall_error_fields(&message) {
5141        let sv = match v {
5142            SysField::Str(s) => h.new_str(s),
5143            SysField::Num(n) => Value::Float(n),
5144        };
5145        props.insert(k.into(), sv);
5146    }
5147    let obj = h.new_object(props);
5148    if let Some(p) = host::error_proto_of(h, &name) {
5149        h.set_proto(&obj, p);
5150    }
5151    // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5152    // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5153    h.hide_prop(&obj, "message");
5154    h.hide_prop(&obj, "stack");
5155    obj
5156}
5157
5158enum SysField {
5159    Str(String),
5160    Num(f64),
5161}
5162
5163/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5164/// own properties Node hangs off a system error. Returns empty for any message
5165/// that is not in that shape.
5166fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5167    let (code, rest) = match message.split_once(": ") {
5168        Some((c, r))
5169            if c.len() >= 2
5170                && c.starts_with('E')
5171                && c.bytes()
5172                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5173        {
5174            (c, r)
5175        }
5176        _ => return Vec::new(),
5177    };
5178    let mut out: Vec<(&'static str, SysField)> = vec![
5179        ("errno", SysField::Num(errno_for(code))),
5180        ("code", SysField::Str(code.to_string())),
5181    ];
5182    // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5183    if let Some((_, tail)) = rest.split_once(", ") {
5184        let (syscall, path) = match tail.split_once(" '") {
5185            // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5186            // one, so the scan stops at its closing quote rather than at the
5187            // end of the line — which had been swallowing `' -> 'dest` into the
5188            // path for every `rename` and `copyFile` failure.
5189            Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5190            None => (tail, None),
5191        };
5192        out.push(("syscall", SysField::Str(syscall.to_string())));
5193        if let Some(p) = path {
5194            out.push(("path", SysField::Str(p.to_string())));
5195        }
5196    }
5197    out
5198}
5199
5200/// The negative `errno` Node reports for a libuv error code on this platform.
5201/// Only the codes `err_str` can produce are mapped; anything else reports the
5202/// generic `EIO` number rather than inventing a value.
5203fn errno_for(code: &str) -> f64 {
5204    let n: i32 = match code {
5205        "ENOENT" => 2,
5206        "EACCES" => 13,
5207        "EEXIST" => 17,
5208        "ENOTDIR" => 20,
5209        "EISDIR" => 21,
5210        "EINVAL" => 22,
5211        "EPIPE" => 32,
5212        "ENOTEMPTY" => 66,
5213        _ => 5, // EIO
5214    };
5215    -f64::from(n)
5216}
5217
5218// ── iteration ─────────────────────────────────────────────────────────────────
5219
5220fn b_getiter(vm: &mut VM, _: u8) -> Value {
5221    let v = vm.pop();
5222    // A generator is its own iterator (resumed lazily by FORITER).
5223    if with_host(|h| h.is_generator_val(&v)) {
5224        return v;
5225    }
5226    // A Proxy's iterator comes from its traps, materialized eagerly: the
5227    // `lookup_chain` probe below reads the property map a proxy does not have.
5228    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5229        return match crate::proxy::iterate(&v) {
5230            Ok(Some(items)) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5231            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5232            Err(e) => abort(vm, e),
5233        };
5234    }
5235    // Arrays and strings take the direct path below: they have no iterator
5236    // state to preserve and are the hot case, so they must not pay a property
5237    // lookup and a call per loop.
5238    let direct = matches!(
5239        with_host(|h| h.kind_of(&v)),
5240        Some(ObjKind::Array) | Some(ObjKind::Str)
5241    );
5242    // …but only while their `Symbol.iterator` is still reachable. It comes from
5243    // the intrinsic prototype, so replacing the link takes it away: node reports
5244    // `a is not iterable` for an array whose prototype is a plain object, where
5245    // the fast path below iterated the backing vector regardless.
5246    if !own_intrinsic_reachable(&v)
5247        && !matches!(
5248            get_property(&v, "@@iterator"),
5249            Ok(ref f) if with_host(|h| host::is_callable(h, f))
5250        )
5251    {
5252        let shown = with_host(|h| h.inspect(&v));
5253        let msg = host::type_error(&format!("{shown} is not iterable"));
5254        return abort(vm, host::name_call_site(vm, &shown, msg));
5255    }
5256    // Anything else with a `Symbol.iterator`: call it for the iterator object.
5257    //
5258    // Resolved as a full property READ, not a stored-property lookup. A
5259    // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5260    // dispatches its methods through the stdlib table rather than a property
5261    // map, so a `lookup_chain` probe found nothing and the loop fell through to
5262    // materializing the value — which threw for `URLSearchParams` and
5263    // snapshotted for `Map`. Spreading the same object already worked, because
5264    // that path had been fixed and this one had not.
5265    if !direct {
5266        if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5267            if with_host(|h| host::is_callable(h, &iter_fn)) {
5268                return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5269                    Ok(it) => it,
5270                    Err(e) => abort(vm, e),
5271                };
5272            }
5273        }
5274    }
5275    match with_host(|h| h.iter_vec(&v)) {
5276        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5277        // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5278        // reports `a is not iterable`. The text was recorded for this op.
5279        Err(e) => {
5280            let shown = with_host(|h| h.inspect(&v));
5281            let named = host::name_call_site(vm, &shown, e);
5282            abort(vm, named)
5283        }
5284    }
5285}
5286
5287fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5288    let v = vm.pop();
5289    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5290    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5291    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5292    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5293        // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5294        // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5295        // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5296        // the body the way node's do, instead of all firing up front.
5297        return match crate::proxy::own_keys(&v) {
5298            Ok(keys) => with_host(|h| {
5299                let out: Vec<Value> = keys
5300                    .unwrap_or_default()
5301                    .into_iter()
5302                    .filter(|k| !host::is_symbol_key(k))
5303                    .map(|k| h.new_str(k))
5304                    .collect();
5305                h.new_array(out)
5306            }),
5307            Err(e) => abort(vm, e),
5308        };
5309    }
5310    let mut keys = with_host(|h| h.enum_keys(&v));
5311    // A member patched onto the receiver's INTRINSIC prototype is enumerable
5312    // and inherited, so `for-in` visits it after the own keys — but the
5313    // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5314    // pass never reaches them.
5315    if !with_host(|h| h.has_null_proto(&v)) {
5316        let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5317        for ns in intrinsic_proto_namespaces(&v) {
5318            for k in with_host(|h| h.builtin_static_keys(&ns)) {
5319                if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5320                    keys.push(with_host(|h| h.new_str(k)));
5321                }
5322            }
5323        }
5324    }
5325    with_host(|h| h.new_array(keys))
5326}
5327
5328/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5329/// own constructor's and then `Object`'s, the same two steps
5330/// `inherited_builtin_static` looks a value up in.
5331fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5332    let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5333        Some(c) => Some(c),
5334        None if is_arguments(v) => Some("Object"),
5335        None => with_host(|h| default_ctor_name(h, v)),
5336    };
5337    let mut out: Vec<String> = ctor
5338        .filter(|c| *c != "Object")
5339        .map(|c| format!("{c}.prototype"))
5340        .into_iter()
5341        .collect();
5342    out.push("Object.prototype".to_string());
5343    out
5344}
5345
5346/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5347///
5348/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5349/// of the enumerable keys is observationally the same for everything except
5350/// this), and the body can delete a key before the loop reaches it. Node does
5351/// not visit a key deleted that way; without this check `delete d.z` inside the
5352/// loop still produced `x,y,z`.
5353///
5354/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5355/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5356/// `has` trap, which node never fires for `for-in`. That also puts each trap
5357/// call immediately before its visit, matching node's interleaving — the trap
5358/// log used to show every `gopd` up front because the key list was filtered
5359/// eagerly.
5360fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5361    let key = vm.pop();
5362    let obj = vm.pop();
5363    let name = with_host(|h| h.str_of(&key));
5364    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5365        return match crate::proxy::own_enumerable(&obj, &name) {
5366            Ok(b) => Value::Bool(b),
5367            Err(e) => abort(vm, e),
5368        };
5369    }
5370    // A STRING's keys are its character indices. `in` is not defined on a string
5371    // primitive at all, so the ordinary path below has no answer for one and
5372    // `for (const i in 'abc')` came back empty.
5373    if let Some(s) = with_host(|h| h.as_str(&obj)) {
5374        let len = crate::utf16::len(&s);
5375        return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5376    }
5377    // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5378    // there and does NOT re-check enumerability — making one non-enumerable
5379    // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5380    // (The Proxy branch above does re-check, because there the answer comes from
5381    // the trap node itself calls.)
5382    Value::Bool(has_property_ordinary(&obj, &name))
5383}
5384
5385fn b_foriter(vm: &mut VM, _: u8) -> Value {
5386    let it = match vm.stack.last() {
5387        Some(v) => v.clone(),
5388        None => return abort(vm, "internal: FORITER with empty stack".into()),
5389    };
5390    // Eager array-backed iterator (arrays/strings/Map/Set).
5391    let eager = with_host(|h| {
5392        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
5393            if *idx < items.len() {
5394                let v = items[*idx].clone();
5395                *idx += 1;
5396                return Some(Some(v));
5397            }
5398            return Some(None);
5399        }
5400        None
5401    });
5402    if let Some(step) = eager {
5403        return match step {
5404            Some(v) => {
5405                vm.push(v);
5406                Value::Bool(true)
5407            }
5408            None => Value::Bool(false),
5409        };
5410    }
5411    // Generator: resume one step.
5412    if with_host(|h| h.is_generator_val(&it)) {
5413        return match host::gen_resume(&it, Value::Undef) {
5414            Ok(host::GenStep::Yield(v)) => {
5415                vm.push(v);
5416                Value::Bool(true)
5417            }
5418            Ok(host::GenStep::Done(_)) => Value::Bool(false),
5419            Err(e) => abort(vm, e),
5420        };
5421    }
5422    // A user iterator object with a `.next()` returning `{ value, done }`.
5423    match host::call_method(&it, "next", Vec::new()) {
5424        Ok(step) => {
5425            let done = get_property(&step, "done")
5426                .map(|d| with_host(|h| h.truthy(&d)))
5427                .unwrap_or(true);
5428            if done {
5429                Value::Bool(false)
5430            } else {
5431                match get_property(&step, "value") {
5432                    Ok(v) => {
5433                        vm.push(v);
5434                        Value::Bool(true)
5435                    }
5436                    Err(e) => abort(vm, e),
5437                }
5438            }
5439        }
5440        Err(e) => abort(vm, e),
5441    }
5442}
5443
5444fn b_unpack(vm: &mut VM, _: u8) -> Value {
5445    let star = match vm.pop() {
5446        Value::Int(n) => n,
5447        _ => -1,
5448    };
5449    let count = match vm.pop() {
5450        Value::Int(n) => n as usize,
5451        _ => 0,
5452    };
5453    let iterable = vm.pop();
5454    // Without a `...rest` element the pattern needs exactly `count` values and
5455    // must then close the iterator; draining hung on an unbounded source.
5456    let items = match if star < 0 {
5457        host::iter_take(&iterable, count)
5458    } else {
5459        host::iter_all(&iterable)
5460    } {
5461        Ok(v) => v,
5462        // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5463        // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5464        // was recorded for this op at compile time.
5465        Err(e) => {
5466            // Node names the source only when the pattern's right-hand side is
5467            // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5468            // Anything else (a member, a call, a nested pattern, a parameter)
5469            // reports the TYPE instead, with the property note. Measured across
5470            // twelve shapes rather than guessed.
5471            let msg = match host::call_site_text(vm) {
5472                Some(text) => host::type_error(&format!("{text} is not iterable")),
5473                None if e.ends_with(" is not iterable") => {
5474                    host::type_error(&not_iterable_typed(&iterable))
5475                }
5476                None => e,
5477            };
5478            return abort(vm, msg);
5479        }
5480    };
5481    let ordered: Vec<Value> = if star < 0 {
5482        (0..count)
5483            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5484            .collect()
5485    } else {
5486        let si = star as usize;
5487        let after = count.saturating_sub(si + 1);
5488        let rest_end = items.len().saturating_sub(after).max(si);
5489        let mut out: Vec<Value> = Vec::with_capacity(count);
5490        for i in 0..si {
5491            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5492        }
5493        let rest: Vec<Value> = items
5494            .get(si..rest_end)
5495            .map(|s| s.to_vec())
5496            .unwrap_or_default();
5497        out.push(with_host(|h| h.new_array(rest)));
5498        for j in 0..after {
5499            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5500        }
5501        out
5502    };
5503    if ordered.is_empty() {
5504        return Value::Undef;
5505    }
5506    for it in ordered[1..].iter().rev().cloned() {
5507        vm.push(it);
5508    }
5509    ordered[0].clone()
5510}
5511
5512fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5513    let flat = pop_n(vm, argc as usize);
5514    let mut out = Vec::new();
5515    // Elided positions of an array literal (tag 2), recorded as the run-time
5516    // index each lands on — which only this walk knows, because a preceding
5517    // spread contributes an unknown number of elements. Call-argument lists,
5518    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5519    // empty for them.
5520    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5521    let mut i = 0;
5522    while i + 1 < flat.len() {
5523        let val = flat[i + 1].clone();
5524        match flat[i] {
5525            // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5526            // report a non-iterable differently, which is the only reason the
5527            // two are told apart here.
5528            Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5529                let shown = with_host(|h| h.inspect(&val));
5530                host::name_call_site(vm, &shown, e)
5531            }) {
5532                Ok(items) => out.extend(items),
5533                Err(e) => return abort(vm, e),
5534            },
5535            Value::Int(3) => match host::iter_all(&val) {
5536                Ok(items) => out.extend(items),
5537                Err(e) => {
5538                    // A NULLISH spread names the value and what could not be
5539                    // read off it; anything else names the missing protocol.
5540                    let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5541                    return abort(
5542                        vm,
5543                        match shown {
5544                            Some(s) => host::type_error(&format!(
5545                                "{s} is not iterable (cannot read property {s})"
5546                            )),
5547                            None if e.ends_with(" is not iterable") => host::type_error(
5548                                "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5549                            ),
5550                            None => e,
5551                        },
5552                    );
5553                }
5554            },
5555            Value::Int(2) => {
5556                holes.insert(out.len());
5557                out.push(Value::Undef);
5558            }
5559            _ => out.push(val),
5560        }
5561        i += 2;
5562    }
5563    with_host(|h| {
5564        let arr = h.new_array(out);
5565        h.install_holes(&arr, holes);
5566        arr
5567    })
5568}
5569
5570// ── calls ──────────────────────────────────────────────────────────────────────
5571
5572fn b_call(vm: &mut VM, argc: u8) -> Value {
5573    let mut args = pop_n(vm, argc as usize);
5574    let name = sval(&args.remove(0));
5575    let r = host::call_named(&name, args);
5576    // A bare name that resolved to a non-callable reports the VALUE
5577    // (`undefined is not a function`); node names the identifier. Resolving it
5578    // again to learn what the message said costs nothing off the error path.
5579    let r = r.map_err(|e| {
5580        let shown = global_binding(&name)
5581            .map(|v| with_host(|h| h.str_of(&v)))
5582            .unwrap_or_default();
5583        host::name_call_site(vm, &shown, e)
5584    });
5585    finish(vm, r)
5586}
5587
5588/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5589/// method name. `call_method` resolves by name and bottoms out in
5590/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5591/// that happens to be a function reported "is not a function". Read the element
5592/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5593/// A computed call's key is a property key, so it goes through ToPropertyKey:
5594/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5595/// answers "" for a number, which turned `arr[0]()` into a call to the method
5596/// named "" — so the key is stringified here instead.
5597fn call_key_of(v: &Value) -> String {
5598    if let Value::Str(s) = v {
5599        return (**s).clone();
5600    }
5601    // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5602    // spelling that `str_of` does not produce — it renders
5603    // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5604    // method by that display text and reported it was not a function, for every
5605    // object including a plain literal with a computed symbol method. Reading
5606    // the same property without calling it worked, which is what hid this.
5607    with_host(|h| h.property_key(v))
5608}
5609
5610fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5611    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5612        return None;
5613    }
5614    let f = get_property(recv, name).ok()?;
5615    with_host(|h| host::is_callable(h, &f))
5616        .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5617}
5618
5619fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5620    let mut args = pop_n(vm, argc as usize);
5621    let recv = args.remove(0);
5622    let name = call_key_of(&args.remove(0));
5623    if let Some(r) = index_element_call(&recv, &name, &args) {
5624        return finish(vm, r);
5625    }
5626    let r = host::call_method(&recv, &name, args);
5627    // `z.f()` on a missing method is `z.f is not a function` in node, not
5628    // `f is not a function`: V8 names the callee as the source wrote it. The
5629    // text was recorded for this op at compile time.
5630    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5631    finish(vm, r)
5632}
5633
5634fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5635    let mut args = pop_n(vm, argc as usize);
5636    let callable = args.remove(0);
5637    let r = host::invoke(&callable, args, None);
5638    // The callee here is an expression, not a name, so the message it produced
5639    // describes the VALUE (`undefined is not a function`); node names the
5640    // expression. Same site table, keyed on that rendering.
5641    let r = r.map_err(|e| {
5642        let shown = with_host(|h| h.str_of(&callable));
5643        host::name_call_site(vm, &shown, e)
5644    });
5645    finish(vm, r)
5646}
5647
5648/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5649/// rather than a fixed count of stack slots.
5650///
5651/// `compile_new` used to compile each argument with `compile_expr`, and a
5652/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5653/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5654/// Invalid Date.
5655fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5656    let args_arr = vm.pop();
5657    let ctor = vm.pop();
5658    let args = host::iter_all(&args_arr).unwrap_or_default();
5659    let r = host::construct(&ctor, args).map_err(|e| {
5660        let shown = with_host(|h| h.str_of(&ctor));
5661        host::name_call_site(vm, &shown, e)
5662    });
5663    finish(vm, r)
5664}
5665
5666fn b_new(vm: &mut VM, argc: u8) -> Value {
5667    let mut args = pop_n(vm, argc as usize);
5668    let ctor = args.remove(0);
5669    let r = host::construct(&ctor, args);
5670    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5671    // call does.
5672    let r = r.map_err(|e| {
5673        let shown = with_host(|h| h.str_of(&ctor));
5674        host::name_call_site(vm, &shown, e)
5675    });
5676    finish(vm, r)
5677}
5678
5679fn b_apply(vm: &mut VM, _: u8) -> Value {
5680    let args_arr = vm.pop();
5681    let callable = vm.pop();
5682    let args = host::iter_all(&args_arr).unwrap_or_default();
5683    let r = host::invoke(&callable, args, None);
5684    finish(vm, r)
5685}
5686
5687fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5688    let args_arr = vm.pop();
5689    let name = call_key_of(&vm.pop());
5690    let recv = vm.pop();
5691    let args = host::iter_all(&args_arr).unwrap_or_default();
5692    if let Some(r) = index_element_call(&recv, &name, &args) {
5693        return finish(vm, r);
5694    }
5695    let r = host::call_method(&recv, &name, args);
5696    finish(vm, r)
5697}
5698
5699// ── numeric hook ──────────────────────────────────────────────────────────────
5700
5701/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5702/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5703///
5704/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5705/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5706/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5707/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5708/// the VM and `arith` runs under the host's `RefCell` borrow.
5709pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5710    use NumOp::*;
5711    let (a, b) = match op {
5712        // `==`/`!=` only convert when the OTHER side is a primitive that can be
5713        // compared numerically or textually; `{} == {}` stays a reference check.
5714        Eq | Ne => {
5715            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5716            match (pa, pb) {
5717                (false, true) if coerces_against_object(b) => {
5718                    (host::to_primitive(a, "default")?, b.clone())
5719                }
5720                (true, false) if coerces_against_object(a) => {
5721                    (a.clone(), host::to_primitive(b, "default")?)
5722                }
5723                _ => (a.clone(), b.clone()),
5724            }
5725        }
5726        // `+` uses the default hint (`valueOf` first, but a string result still
5727        // selects concatenation); everything else uses the number hint.
5728        Add => (
5729            host::to_primitive(a, "default")?,
5730            host::to_primitive(b, "default")?,
5731        ),
5732        _ => (
5733            host::to_primitive(a, "number")?,
5734            host::to_primitive(b, "number")?,
5735        ),
5736    };
5737    reject_symbol_operand(op, &a, &b)?;
5738    with_host(|h| h.arith(op, &a, &b))
5739}
5740
5741/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5742/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5743/// concatenated `Symbol(desc)` into the result.
5744///
5745/// Which of the two messages V8 uses is decided by whether the operation is
5746/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5747/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5748/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5749/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5750/// (`Symbol() == 1` is `false`), so they are left alone.
5751fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5752    use NumOp::*;
5753    if matches!(op, Eq | Ne) {
5754        return Ok(());
5755    }
5756    let (sym, concat) = with_host(|h| {
5757        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5758        let is_str =
5759            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5760        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5761    });
5762    if !sym {
5763        return Ok(());
5764    }
5765    Err(host::type_error(if matches!(op, Add) && concat {
5766        "Cannot convert a Symbol value to a string"
5767    } else {
5768        "Cannot convert a Symbol value to a number"
5769    }))
5770}
5771
5772/// Whether a primitive `v` makes `==` against an object convert that object
5773/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5774/// `undefined` and booleans are settled without a `ToPrimitive` call
5775/// (a boolean is coerced to a number first, and then it does).
5776fn coerces_against_object(v: &Value) -> bool {
5777    match v {
5778        Value::Undef => false,
5779        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5780        _ => with_host(|h| !h.is_null(v)),
5781    }
5782}
5783
5784// ══ standard library ═══════════════════════════════════════════════════════════
5785
5786/// Namespaces reachable as bare globals.
5787fn is_namespace(name: &str) -> bool {
5788    matches!(
5789        name,
5790        "console"
5791            | "Math"
5792            | "JSON"
5793            | "Object"
5794            | "Array"
5795            | "Number"
5796            | "String"
5797            | "Boolean"
5798            | "Symbol"
5799            | "Reflect"
5800            | "Promise"
5801            | "process"
5802            | "Buffer"
5803            | "URL"
5804            | "URLSearchParams"
5805    )
5806}
5807
5808const GLOBAL_FUNCS: &[&str] = &[
5809    "parseInt",
5810    "parseFloat",
5811    "isNaN",
5812    "isFinite",
5813    "encodeURIComponent",
5814    "decodeURIComponent",
5815    "encodeURI",
5816    "decodeURI",
5817    // Annex B legacy encoders. Still globals on every engine, and still called
5818    // by pre-`encodeURIComponent` library code.
5819    "escape",
5820    "unescape",
5821    "eval",
5822    "String",
5823    "Number",
5824    "Boolean",
5825    "Array",
5826    "Object",
5827    "Function",
5828    "Symbol",
5829    "Map",
5830    "Set",
5831    "WeakMap",
5832    "WeakSet",
5833    "Promise",
5834    "Error",
5835    "TypeError",
5836    "RangeError",
5837    "SyntaxError",
5838    "ReferenceError",
5839    "EvalError",
5840    "URIError",
5841    "AggregateError",
5842    "DOMException",
5843    "Iterator",
5844    "BigInt",
5845    "RegExp",
5846    "Date",
5847    "ArrayBuffer",
5848    "DataView",
5849    "Uint8Array",
5850    "Int8Array",
5851    "Uint8ClampedArray",
5852    "Int16Array",
5853    "Uint16Array",
5854    "Int32Array",
5855    "Uint32Array",
5856    "Float32Array",
5857    "Float64Array",
5858    "BigInt64Array",
5859    "BigUint64Array",
5860    "WeakRef",
5861    "FinalizationRegistry",
5862    "TextEncoder",
5863    "TextDecoder",
5864    // WHATWG Fetch globals (see `stdlib::fetch`).
5865    "fetch",
5866    "Headers",
5867    "Request",
5868    "Response",
5869    "Blob",
5870    "File",
5871    "FormData",
5872    "AbortController",
5873    "AbortSignal",
5874    "queueMicrotask",
5875    "setTimeout",
5876    "setInterval",
5877    "setImmediate",
5878    "clearTimeout",
5879    "clearInterval",
5880    "clearImmediate",
5881    "structuredClone",
5882    // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5883    // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5884    "btoa",
5885    "atob",
5886    "Proxy",
5887    "require",
5888    // CommonJS loader dispatch targets referenced by per-module `require`
5889    // closures (see `module.rs`); never written by user code.
5890    "__cjs_require",
5891    "__cjs_resolve",
5892    "__cjs_cache",
5893];
5894
5895const NS_METHODS: &[&str] = &[
5896    "console.log",
5897    "console.error",
5898    "console.warn",
5899    "console.info",
5900    "console.debug",
5901    "Math.abs",
5902    "Math.acos",
5903    "Math.acosh",
5904    "Math.asin",
5905    "Math.asinh",
5906    "Math.atan",
5907    "Math.atanh",
5908    "Math.atan2",
5909    "Math.ceil",
5910    "Math.cbrt",
5911    "Math.expm1",
5912    "Math.clz32",
5913    "Math.cos",
5914    "Math.cosh",
5915    "Math.exp",
5916    "Math.floor",
5917    "Math.fround",
5918    "Math.hypot",
5919    "Math.imul",
5920    "Math.log",
5921    "Math.log1p",
5922    "Math.log2",
5923    "Math.log10",
5924    "Math.max",
5925    "Math.min",
5926    "Math.pow",
5927    "Math.random",
5928    "Math.round",
5929    "Math.sign",
5930    "Math.sin",
5931    "Math.sinh",
5932    "Math.sqrt",
5933    "Math.tan",
5934    "Math.tanh",
5935    "Math.trunc",
5936    "JSON.stringify",
5937    "JSON.parse",
5938    "JSON.rawJSON",
5939    "JSON.isRawJSON",
5940    "Object.keys",
5941    "Object.values",
5942    "Object.entries",
5943    "Object.assign",
5944    "Object.freeze",
5945    "Object.is",
5946    "Object.fromEntries",
5947    "Object.getPrototypeOf",
5948    "Object.setPrototypeOf",
5949    "Object.create",
5950    "Object.getOwnPropertyNames",
5951    "Object.getOwnPropertySymbols",
5952    "Object.defineProperty",
5953    "Object.getOwnPropertyDescriptor",
5954    "Object.getOwnPropertyDescriptors",
5955    "Object.defineProperties",
5956    "Object.isFrozen",
5957    "Object.isSealed",
5958    "Object.seal",
5959    "Object.preventExtensions",
5960    "Object.isExtensible",
5961    "Object.hasOwn",
5962    "Object.groupBy",
5963    "Array.isArray",
5964    "Array.from",
5965    "Array.fromAsync",
5966    "Array.of",
5967    "Number.isFinite",
5968    "Number.isInteger",
5969    "Number.isNaN",
5970    "Number.isSafeInteger",
5971    "Number.parseFloat",
5972    "Number.parseInt",
5973    "String.fromCharCode",
5974    "String.fromCodePoint",
5975    "String.raw",
5976    "Symbol.for",
5977    "Symbol.keyFor",
5978    "BigInt.asIntN",
5979    "BigInt.asUintN",
5980    "Proxy.revocable",
5981    "Reflect.defineProperty",
5982    "Reflect.deleteProperty",
5983    "Reflect.apply",
5984    "Reflect.construct",
5985    "Reflect.get",
5986    "Reflect.getOwnPropertyDescriptor",
5987    "Reflect.getPrototypeOf",
5988    "Reflect.has",
5989    "Reflect.isExtensible",
5990    "Reflect.ownKeys",
5991    "Reflect.preventExtensions",
5992    "Reflect.set",
5993    "Reflect.setPrototypeOf",
5994    "Promise.resolve",
5995    "Promise.reject",
5996    "Promise.all",
5997    "Promise.allSettled",
5998    "Promise.race",
5999    "Promise.any",
6000    "Promise.withResolvers",
6001    "Promise.try",
6002    "RegExp.escape",
6003    "Error.isError",
6004    "Map.groupBy",
6005    "Response.json",
6006    "Response.error",
6007    "Response.redirect",
6008    "AbortSignal.abort",
6009    "AbortSignal.timeout",
6010    "process.nextTick",
6011    "Error.captureStackTrace",
6012    "require.resolve",
6013    "require.resolve.paths",
6014    "process.memoryUsage.rss",
6015];
6016
6017/// The `name` and `length` a builtin function reports, from the generated
6018/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6019/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6020/// and the core-module functions, whose arity is not specified anywhere.
6021pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6022    crate::arity::BUILTIN_ARITY
6023        .binary_search_by(|(k, _, _)| (*k).cmp(key))
6024        .ok()
6025        .map(|i| {
6026            let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6027            (name, len)
6028        })
6029}
6030
6031/// The `name` a builtin function reports. The table answers for an intrinsic;
6032/// anything else falls back to the last segment of the key, which is what the
6033/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6034/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6035/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6036pub fn builtin_name(key: &str) -> &str {
6037    if let Some((name, _)) = builtin_meta(key) {
6038        return name;
6039    }
6040    match key.strip_prefix("@proto:") {
6041        Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6042        // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6043        // with a `get` prefix), which is what `util.inspect` prints for it and
6044        // what a library reads to identify one.
6045        None => key.rsplit('.').next().unwrap_or(key),
6046    }
6047}
6048
6049/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6050/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6051/// `"get size"` it has to build.
6052pub fn proto_getter_name(key: &str) -> Option<String> {
6053    let (verb, rest) = match key.strip_prefix("@protoget:") {
6054        Some(rest) => ("get", rest),
6055        None => ("set", key.strip_prefix("@protoset:")?),
6056    };
6057    let (_, member) = rest.split_once(':')?;
6058    Some(format!("{verb} {member}"))
6059}
6060
6061pub fn is_known_builtin(name: &str) -> bool {
6062    // Binary search over a sorted INDEX of the two tables rather than a scan of
6063    // both. This runs on every call whose callee is a builtin — `call_method`
6064    // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6065    // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6066    // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6067    // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6068    // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6069    // now. The index is built once per process and derived FROM those tables, so
6070    // it cannot drift from them.
6071    //
6072    // That is an operation count, not a measured time, and NO wall-clock win is
6073    // claimed. Re-measured in isolation (this hunk alone applied to the previous
6074    // commit, interleaved against it, minimums over ten rounds each): the A/B
6075    // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6076    // the A/A control — the SAME binary under both labels — came out 1.084,
6077    // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6078    // this machine the change is not distinguishable from noise. It is kept for
6079    // the comparison count and because it cannot drift from the tables it is
6080    // derived from, not because anything got faster.
6081    static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6082    let sorted = SORTED.get_or_init(|| {
6083        let mut v: Vec<&'static str> = GLOBAL_FUNCS
6084            .iter()
6085            .chain(NS_METHODS.iter())
6086            .copied()
6087            .collect();
6088        v.sort_unstable();
6089        v
6090    });
6091    sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6092}
6093
6094// ── dynamic functions (runtime source → callable) ────────────────────────────
6095
6096/// Build a callable from a complete function-expression source text — the ONE
6097/// dynamic-function generator on this frontend.
6098///
6099/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6100/// wrapping parentheses needed to parse it as an expression: those are added
6101/// here, and `src` itself is retained so `Function.prototype.toString` reports
6102/// what V8 reports. The two callers synthesize different text and both shapes
6103/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6104///
6105/// The body runs in the MODULE scope, never the constructing function's scope
6106/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6107/// *global* environment). That also makes a `var` inside the body a function
6108/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6109/// returns 6 and leaves `globalThis.zz` `undefined`.
6110pub fn dynamic_function(src: &str) -> Result<Value, String> {
6111    let f = crate::eval_in_global_scope(&format!("({src})"))?;
6112    with_host(|h| {
6113        let s = h.new_str(src.to_string());
6114        h.set_fn_prop(&f, "@@source", s);
6115    });
6116    Ok(f)
6117}
6118
6119/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6120///
6121/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6122/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6123/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6124/// arguments at all, both the parameter list and the body are empty.
6125///
6126/// Measured on node v26.7.0:
6127///
6128/// ```text
6129/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6130/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
6131/// new Function('a,b','c','return [a,b,c]').length === 3
6132/// new Function('a','b','return a+b').name       === 'anonymous'
6133/// ```
6134pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6135    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6136    let (params, body) = match parts.split_last() {
6137        Some((body, params)) => (params.join(","), body.clone()),
6138        None => (String::new(), String::new()),
6139    };
6140    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6141}
6142
6143/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6144/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6145/// other route to the same function value is an INDIRECT eval and evaluates in
6146/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6147/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6148/// do not.
6149///
6150/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6151pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6152    let v = arg.cloned().unwrap_or(Value::Undef);
6153    let is_string =
6154        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6155    if !is_string {
6156        return Ok(v);
6157    }
6158    let src = with_host(|h| h.str_of(&v));
6159    // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6160    // decides both the early errors the COMPILE raises and the variable
6161    // environment below. An INDIRECT one is global-scope sloppy code.
6162    let caller_strict = direct && with_host(|h| h.current_strict());
6163    let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6164    if !direct {
6165        return host::run_chunk_in_global_scope(chunk);
6166    }
6167    // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6168    // so its `var`s and function declarations die with it. Only a SLOPPY one
6169    // shares the caller's, which is the form that can inject a binding — and
6170    // sharing it unconditionally meant `eval('var x=1')` inside strict code
6171    // left `x` behind.
6172    let strict = caller_strict
6173        || src.trim_start().starts_with("'use strict'")
6174        || src.trim_start().starts_with("\"use strict\"");
6175    if !strict {
6176        // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6177        // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6178        // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6179        // or `class` declared inside therefore dies with the eval; every one of
6180        // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6181        // behind and `let a=1; eval('let a=2')` overwrote it.
6182        //
6183        // `push_scope` is exactly that split: `var` and a hoisted function
6184        // declaration bind to `base_env`, which this does not touch.
6185        with_host(|h| h.push_scope());
6186        let out = host::run_chunk_on(chunk);
6187        with_host(|h| h.pop_scope());
6188        return out;
6189    }
6190    let prev = with_host(|h| h.push_var_scope());
6191    let out = host::run_chunk_on(chunk);
6192    with_host(|h| h.pop_var_scope(prev));
6193    out
6194}
6195
6196/// Call a resolved builtin function (global or `namespace.method`).
6197pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6198    // `require(spec)`: the ENTRY script's top-level require — core module first,
6199    // else the CommonJS loader resolving from the entry file's directory.
6200    if name == "require" {
6201        let spec = with_host(|h| h.str_of(&arg0(&args)));
6202        return crate::module::require(&spec, &crate::module::entry_dir());
6203    }
6204    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6205    // into the loader, resolving `spec` against the module's own directory.
6206    if name == "__cjs_require" {
6207        let spec = with_host(|h| h.str_of(&arg0(&args)));
6208        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6209        return crate::module::require(&spec, std::path::Path::new(&from));
6210    }
6211    if name == "process.memoryUsage.rss" {
6212        return Ok(crate::stdlib::process::memory_usage_rss());
6213    }
6214    if name == "require.resolve.paths" {
6215        let spec = with_host(|h| h.str_of(&arg0(&args)));
6216        // A core module is not looked up on disk at all.
6217        if crate::stdlib::is_core(&spec) {
6218            return Ok(with_host(|h| h.null()));
6219        }
6220        let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6221        return Ok(with_host(|h| {
6222            let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6223            h.new_array(items)
6224        }));
6225    }
6226    // A `require.extensions` entry. This runtime's loader does not dispatch
6227    // through the map, so calling one is the loader's own behaviour for that
6228    // extension rather than a hook point.
6229    if let Some(ext) = name.strip_prefix("@@extension:") {
6230        let _ = ext;
6231        return Ok(Value::Undef);
6232    }
6233    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6234    if name == "require.resolve" {
6235        let spec = with_host(|h| h.str_of(&arg0(&args)));
6236        if crate::stdlib::is_core(&spec) {
6237            return Ok(with_host(|h| h.new_str(spec)));
6238        }
6239        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6240            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6241            None => Err(crate::host::plain_coded_error(
6242                "Error",
6243                "MODULE_NOT_FOUND",
6244                &format!("Cannot find module '{spec}'"),
6245            )),
6246        };
6247    }
6248    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6249    // path (core modules resolve to the bare specifier, as in Node).
6250    if name == "__cjs_resolve" {
6251        let spec = with_host(|h| h.str_of(&arg0(&args)));
6252        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6253        if crate::stdlib::is_core(&spec) {
6254            return Ok(with_host(|h| h.new_str(spec)));
6255        }
6256        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6257            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6258            None => Err(crate::host::plain_coded_error(
6259                "Error",
6260                "MODULE_NOT_FOUND",
6261                &format!("Cannot find module '{spec}'"),
6262            )),
6263        };
6264    }
6265    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6266    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6267    // stack-introspection pattern used by `depd`), it is called with a synthetic
6268    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6269    if name == "Error.captureStackTrace" {
6270        let target = arg0(&args);
6271        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6272        let stack = match prep {
6273            Some(f)
6274                if matches!(
6275                    with_host(|h| h.get(&f).cloned()),
6276                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6277                ) =>
6278            {
6279                let sites = crate::module::callsite_stack(10)?;
6280                host::invoke(&f, vec![target.clone(), sites], None)?
6281            }
6282            _ => with_host(|h| h.new_str("")),
6283        };
6284        let _ = set_property(&target, "stack", stack);
6285        return Ok(Value::Undef);
6286    }
6287    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6288    if let Some(r) = crate::stdlib::call(name, &args) {
6289        return r;
6290    }
6291    match name {
6292        // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6293        // followed by one `    at <site>` line per call site. Reachable because
6294        // the read hands the hook out, and a library may call it directly to
6295        // render a stack it captured.
6296        DEFAULT_PREPARE => {
6297            let err = arg0(&args);
6298            let header = with_host(|h| {
6299                let name = host::lookup_chain(h, &err, "name")
6300                    .map(|v| h.str_of(&v))
6301                    .unwrap_or_else(|| "Error".to_string());
6302                let msg = host::lookup_chain(h, &err, "message")
6303                    .map(|v| h.str_of(&v))
6304                    .unwrap_or_default();
6305                if msg.is_empty() {
6306                    name
6307                } else {
6308                    format!("{name}: {msg}")
6309                }
6310            });
6311            let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6312            let lines = with_host(|h| match h.get(&sites) {
6313                Some(JsObj::Array(items)) => items.clone(),
6314                _ => Vec::new(),
6315            });
6316            let mut out = header;
6317            for s in lines {
6318                let rendered = host::to_string_value(&s)
6319                    .map(|v| with_host(|h| h.str_of(&v)))
6320                    .unwrap_or_default();
6321                out.push_str("\n    at ");
6322                out.push_str(&rendered);
6323            }
6324            Ok(with_host(|h| h.new_str(out)))
6325        }
6326        "console.log" | "console.info" | "console.debug" => {
6327            print_line(&args, false)?;
6328            Ok(Value::Undef)
6329        }
6330        "console.error" | "console.warn" => {
6331            print_line(&args, true)?;
6332            Ok(Value::Undef)
6333        }
6334        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6335        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6336        // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6337        "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6338        "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6339        "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6340        "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6341        "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6342        "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6343        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6344        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6345        // Reaching `eval` through this table means the eval FUNCTION VALUE was
6346        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6347        // Those are INDIRECT evals and run in the global scope. A literal
6348        // `eval(src)` is intercepted earlier, in `host::call_named`.
6349        "eval" => eval_source(args.first(), false),
6350        // `new Function(...)` and `Function(...)` are the same operation
6351        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6352        // [[Construct]]), so both route to the one generator.
6353        "Function" => function_ctor(&args),
6354        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6355        // (DEP0005). Node still supports it and still routes it to the same place
6356        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6357        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6358        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6359        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6360        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6361        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6362        // where Node warns.
6363        "Buffer" => {
6364            crate::stdlib::process::emit_deprecation_warning(
6365                "DEP0005",
6366                "Buffer() is deprecated due to security and usability issues. \
6367                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6368                 Buffer.from() methods instead.",
6369            );
6370            crate::stdlib::construct("Buffer", &args)
6371                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6372        }
6373        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6374        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6375        "Number.isNaN" => Ok(Value::Bool(
6376            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6377        )),
6378        "Number.isFinite" => Ok(Value::Bool(
6379            matches!(arg0(&args), Value::Float(f) if f.is_finite())
6380                || matches!(arg0(&args), Value::Int(_)),
6381        )),
6382        "String" => {
6383            if args.is_empty() {
6384                Ok(with_host(|h| h.new_str("")))
6385            } else {
6386                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6387                // is allowed); everything else via ToString method dispatch.
6388                host::string_ctor_value(&args[0])
6389            }
6390        }
6391        // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6392        // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6393        // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6394        // one — which is why this cannot just call `to_number_value`.
6395        "Number" => Ok(Value::Float(if args.is_empty() {
6396            0.0
6397        } else {
6398            let prim = host::to_primitive(&args[0], "number")?;
6399            match with_host(|h| h.as_bigint(&prim)) {
6400                Some(b) => host::bigint_to_f64(&b),
6401                None => host::to_number_value(&prim)?,
6402            }
6403        })),
6404        "BigInt" => bigint_ctor(&arg0(&args)),
6405        "RegExp" => regexp_ctor(&args),
6406        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6407        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6408        // Each argument is truncated to a uint16 and taken as one code UNIT, so
6409        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6410        // a surrogate PAIR of arguments composes into one character.
6411        "String.fromCharCode" => Ok(with_host(|h| {
6412            let units: Vec<u16> = args
6413                .iter()
6414                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6415                .collect();
6416            let s = crate::utf16::to_string_lossy(&units);
6417            h.new_str(s)
6418        })),
6419        // `fromCodePoint` takes whole code POINTS and rejects anything that is
6420        // not one — including a lone surrogate, which `fromCharCode` accepts.
6421        "String.fromCodePoint" => {
6422            let mut s = String::new();
6423            for a in &args {
6424                let n = with_host(|h| h.to_number(a));
6425                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6426                {
6427                    char::from_u32(n as u32)
6428                } else {
6429                    None
6430                };
6431                match cp {
6432                    Some(c) => s.push(c),
6433                    None => {
6434                        return Err(format!(
6435                            "RangeError: Invalid code point {}",
6436                            with_host(|h| h.str_of(a))
6437                        ))
6438                    }
6439                }
6440            }
6441            Ok(new_s(s))
6442        }
6443        "String.raw" => string_raw(&args),
6444        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6445        "Array" => construct_builtin("Array", args),
6446        "Array.of" => construct_array_like(host::current_static_this(), args),
6447        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6448        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6449        "Array.isArray" => {
6450            let v = arg0(&args);
6451            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6452            Ok(Value::Bool(
6453                matches!(
6454                    with_host(|h| h.get(&subject).cloned()),
6455                    Some(JsObj::Array(_))
6456                ) && !is_arguments(&subject),
6457            ))
6458        }
6459        "Array.from" => array_from(args),
6460        "Array.fromAsync" => array_from_async(args),
6461        "Object" => Ok(object_call(args)),
6462        "Object.keys" => object_keys(args, 0),
6463        "Object.values" => object_keys(args, 1),
6464        "Object.entries" => object_keys(args, 2),
6465        "Object.assign" => object_assign(args),
6466        "Object.freeze" => {
6467            let v = arg0(&args);
6468            reject_sealing_a_view(&v, "freeze")?;
6469            if seal_proxy(&v, true)? {
6470                return Ok(v);
6471            }
6472            with_host(|h| h.seal_object(&v, true));
6473            Ok(v)
6474        }
6475        "Object.seal" => {
6476            let v = arg0(&args);
6477            reject_sealing_a_view(&v, "seal")?;
6478            if seal_proxy(&v, false)? {
6479                return Ok(v);
6480            }
6481            with_host(|h| h.seal_object(&v, false));
6482            Ok(v)
6483        }
6484        "Object.preventExtensions" => {
6485            let v = arg0(&args);
6486            if crate::proxy::prevent_extensions(&v)? {
6487                return Ok(v);
6488            }
6489            with_host(|h| h.prevent_extensions(&v));
6490            Ok(v)
6491        }
6492        // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6493        // without coercion: it is not extensible, and vacuously frozen and
6494        // sealed. Reporting it extensible and unfrozen was the opposite of
6495        // every one of the three.
6496        "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6497        "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6498        "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6499        "Object.isFrozen" => integrity_level(&arg0(&args), true),
6500        "Object.isSealed" => integrity_level(&arg0(&args), false),
6501        "Object.isExtensible" => {
6502            let v = arg0(&args);
6503            match crate::proxy::is_extensible(&v)? {
6504                Some(b) => Ok(Value::Bool(b)),
6505                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6506            }
6507        }
6508        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6509        // distinct from -0.
6510        "Object.is" => {
6511            let a = arg0(&args);
6512            let b = args.get(1).cloned().unwrap_or(Value::Undef);
6513            let num = |v: &Value| match v {
6514                Value::Int(n) => Some(*n as f64),
6515                Value::Float(f) => Some(*f),
6516                _ => None,
6517            };
6518            let r = match (num(&a), num(&b)) {
6519                (Some(x), Some(y)) => {
6520                    if x.is_nan() && y.is_nan() {
6521                        true
6522                    } else if x == 0.0 && y == 0.0 {
6523                        x.is_sign_negative() == y.is_sign_negative()
6524                    } else {
6525                        x == y
6526                    }
6527                }
6528                _ => with_host(|h| h.strict_eq(&a, &b)),
6529            };
6530            Ok(Value::Bool(r))
6531        }
6532        "Object.fromEntries" => object_from_entries(args),
6533        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6534        // so the proxy form cannot share `prototype_of`'s infallible signature.
6535        // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6536        // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6537        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6538            if name == "Reflect.getPrototypeOf" {
6539                reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6540            }
6541            let v = arg0(&args);
6542            match crate::proxy::get_prototype_of(&v)? {
6543                Some(p) => Ok(p),
6544                None => Ok(prototype_of(&v)),
6545            }
6546        }
6547        "Object.setPrototypeOf" => {
6548            let obj = arg0(&args);
6549            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6550            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6551                reject_bad_prototype(&proto)?;
6552                crate::proxy::set_prototype_of(&obj, &proto)?;
6553                return Ok(obj);
6554            }
6555            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6556            // prototype type check, then — only for an actual object target —
6557            // the extensibility check. A PRIMITIVE target is returned untouched
6558            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6559            // extensibility test cannot come first.
6560            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6561                return Err(host::type_error(
6562                    "Object.setPrototypeOf called on null or undefined",
6563                ));
6564            }
6565            reject_bad_prototype(&proto)?;
6566            if with_host(|h| is_object_like(h, &obj)) {
6567                // Setting the SAME prototype is a no-op and stays legal even on a
6568                // frozen object: node v26.7.0 accepts
6569                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6570                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6571                // link still has `Object.prototype`, and comparing against the
6572                // absent link would call that a change.
6573                if would_cycle(&obj, &proto) {
6574                    return Err(host::type_error("Cyclic __proto__ value"));
6575                }
6576                if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6577                    // The receiver is named by its brand, as every other
6578                    // refusal names it — a NULL-PROTOTYPE object is
6579                    // `[object Object]`, not `#<Object>`, because it has no
6580                    // constructor to name.
6581                    return Err(host::type_error(&format!(
6582                        "{} is not extensible",
6583                        no_side_effects_string(&obj)
6584                    )));
6585                }
6586                with_host(|h| h.set_proto(&obj, proto));
6587            }
6588            Ok(obj)
6589        }
6590        "Object.create" => object_create(args),
6591        "Object.getOwnPropertyNames" => object_keys(args, 3),
6592        "Object.getOwnPropertySymbols" => {
6593            let v = arg0(&args);
6594            require_object_coercible(&v)?;
6595            let syms = proxy_or_own_symbol_keys(&v)?;
6596            Ok(with_host(|h| h.new_array(syms)))
6597        }
6598        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6599        "Object.hasOwn" => {
6600            let obj = arg0(&args);
6601            let key = args.get(1).cloned().unwrap_or(Value::Undef);
6602            object_builtin_method(&obj, "hasOwnProperty", vec![key])
6603        }
6604        "Object.defineProperty" => object_define_property(args),
6605        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6606        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6607        "Object.defineProperties" => object_define_properties(args),
6608        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6609        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6610        "Object.groupBy" => object_group_by(args),
6611        "Symbol" => Ok(with_host(|h| {
6612            let desc = args
6613                .first()
6614                .filter(|a| !matches!(a, Value::Undef))
6615                .map(|a| h.str_of(a));
6616            h.new_symbol(desc)
6617        })),
6618        "Symbol.for" => Ok(with_host(|h| {
6619            let key = h.str_of(&arg0(&args));
6620            h.symbol_for(&key)
6621        })),
6622        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6623        // description read: it answers only for symbols `Symbol.for` created.
6624        // Returning the description made every symbol look registered —
6625        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6626        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6627        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6628        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6629        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6630        "Proxy.revocable" => crate::proxy::revocable(&args),
6631        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6632        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6633        // own properties, so there is no second half to append).
6634        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6635        // non-enumerable included, strings first and then the SYMBOLS.
6636        "Reflect.ownKeys" => {
6637            let v = arg0(&args);
6638            reflect_require_object(&v, "ownKeys")?;
6639            let names = object_keys(args, 3)?;
6640            let syms = proxy_or_own_symbol_keys(&v)?;
6641            if syms.is_empty() {
6642                return Ok(names);
6643            }
6644            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6645            all.extend(syms);
6646            Ok(with_host(|h| h.new_array(all)))
6647        }
6648        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6649        // `Reflect.defineProperty` REPORTS success as a boolean where
6650        // `Object.defineProperty` throws (28.1.3). It was propagating the
6651        // throw, so the whole point of the reflective form was lost.
6652        "Reflect.defineProperty" => {
6653            reflect_require_object(&arg0(&args), "defineProperty")?;
6654            Ok(Value::Bool(object_define_property(args).is_ok()))
6655        }
6656        "Reflect.deleteProperty" => {
6657            let obj = arg0(&args);
6658            reflect_require_object(&obj, "deleteProperty")?;
6659            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6660            Ok(Value::Bool(delete_property(&obj, &k)?))
6661        }
6662        "Reflect.setPrototypeOf" => {
6663            let obj = arg0(&args);
6664            let p = args.get(1).cloned().unwrap_or(Value::Undef);
6665            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6666                crate::proxy::set_prototype_of(&obj, &p)?;
6667                return Ok(Value::Bool(true));
6668            }
6669            // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6670            // unless the new one is what it already has, which is a no-op. It
6671            // reported success and rewrote the link.
6672            // `Reflect` reports a refusal rather than throwing, for a cycle as
6673            // for a non-extensible receiver.
6674            if would_cycle(&obj, &p) {
6675                return Ok(Value::Bool(false));
6676            }
6677            if !with_host(|h| h.is_extensible(&obj)) {
6678                return Ok(Value::Bool(same_prototype(&obj, &p)));
6679            }
6680            with_host(|h| h.set_proto(&obj, p));
6681            Ok(Value::Bool(true))
6682        }
6683        "Reflect.isExtensible" => {
6684            let v = arg0(&args);
6685            match crate::proxy::is_extensible(&v)? {
6686                Some(b) => Ok(Value::Bool(b)),
6687                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6688            }
6689        }
6690        "Reflect.preventExtensions" => {
6691            let v = arg0(&args);
6692            if crate::proxy::prevent_extensions(&v)? {
6693                return Ok(Value::Bool(true));
6694            }
6695            with_host(|h| h.prevent_extensions(&v));
6696            Ok(Value::Bool(true))
6697        }
6698        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6699        "Reflect.apply" => {
6700            let f = arg0(&args);
6701            let this = args.get(1).cloned();
6702            let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6703            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6704        }
6705        // `Reflect.construct(target, args, newTarget)` — the optional third
6706        // argument decides which constructor's `prototype` the instance gets
6707        // (28.1.2). It was ignored, so the result always inherited from
6708        // `target` and `instanceof newTarget` was false.
6709        "Reflect.construct" => {
6710            let f = arg0(&args);
6711            let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6712            let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6713            host::construct_nt(&f, list, new_target)
6714        }
6715        "Reflect.has" => {
6716            let obj = arg0(&args);
6717            reflect_require_object(&obj, "has")?;
6718            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6719            Ok(Value::Bool(has_property(&obj, &k)?))
6720        }
6721        // `Reflect.get(target, key, receiver)` — the optional third argument is
6722        // what a getter sees as `this` (28.1.6). Defaults to the target.
6723        "Reflect.get" => {
6724            let obj = arg0(&args);
6725            reflect_require_object(&obj, "get")?;
6726            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6727            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6728            get_property_recv(&obj, &k, &receiver)
6729        }
6730        // `Reflect.set(target, key, value, receiver)` — the optional fourth
6731        // argument is what a setter sees as `this`, and where a DATA property
6732        // lands (28.1.13). It was ignored: the setter ran against the target
6733        // and the property was written there.
6734        "Reflect.set" => {
6735            let obj = arg0(&args);
6736            reflect_require_object(&obj, "set")?;
6737            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6738            let v = args.get(2).cloned().unwrap_or(Value::Undef);
6739            let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6740            Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6741        }
6742        "JSON.stringify" => json_stringify(args),
6743        "JSON.parse" => json_parse(args),
6744        "JSON.rawJSON" => json_raw(args),
6745        "JSON.isRawJSON" => json_is_raw(args),
6746        "structuredClone" => structured_clone(args),
6747        // The deferred drain a `Readable.from` schedules; the suffix is the
6748        // stream's heap index.
6749        _ if name.starts_with("@@transformCb:") => {
6750            let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6751            crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6752            Ok(Value::Undef)
6753        }
6754        _ if name.starts_with("@@streamFlush:") => {
6755            let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6756            crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6757            Ok(Value::Undef)
6758        }
6759        // Same implementation the `buffer` module exposes; only the binding was
6760        // missing.
6761        "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6762            .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6763        "fetch" => crate::stdlib::fetch::fetch(&args),
6764        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6765        // suffix is the signal's heap index.
6766        _ if name.starts_with("@@aborttimeout:") => {
6767            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6768            crate::stdlib::fetch::fire_timeout_abort(idx)
6769        }
6770        // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6771        // implementation. Nothing here waits on backpressure, so it only has to
6772        // BE callable — an implementation that ends with `cb()`, which the
6773        // stream contract requires, would otherwise throw.
6774        "@@streamWriteCallback" => Ok(Value::Undef),
6775        "queueMicrotask" | "process.nextTick" => {
6776            let cb = arg0(&args);
6777            require_callback(&cb)?;
6778            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6779            enqueue_microtask(name == "process.nextTick", cb, rest);
6780            Ok(Value::Undef)
6781        }
6782        "setTimeout" | "setInterval" | "setImmediate" => {
6783            require_callback(&arg0(&args))?;
6784            Ok(schedule_timer(name, args))
6785        }
6786        "clearTimeout" | "clearInterval" | "clearImmediate" => {
6787            clear_timer(&arg0(&args));
6788            Ok(Value::Undef)
6789        }
6790        "Promise.resolve" => promise_resolve(arg0(&args)),
6791        "Promise.reject" => promise_reject(arg0(&args)),
6792        "Promise.all" => promise_all(args, AllMode::All),
6793        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6794        "Promise.race" => promise_race(args, false),
6795        "Promise.any" => promise_race(args, true),
6796        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6797        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6798        "Promise.withResolvers" => promise_with_resolvers(),
6799        "Promise.try" => promise_try(args),
6800        "RegExp.escape" => regexp_escape(args),
6801        "Error.isError" => error_is_error(args),
6802        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6803        // `cb(item, i)` result (SameValueZero), each value an array of members.
6804        "Map.groupBy" => map_group_by(args),
6805        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6806        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6807        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6808        // The executor a species-constructed promise is built with: it does
6809        // nothing, because the caller settles the result through its id.
6810        "@@pnoop" => Ok(Value::Undef),
6811        _ if name.starts_with("@@presolve:") => {
6812            let id: u32 = name[11..].parse().unwrap_or(0);
6813            host::resolve_promise_val(id, arg0(&args));
6814            Ok(Value::Undef)
6815        }
6816        _ if name.starts_with("@@preject:") => {
6817            let id: u32 = name[10..].parse().unwrap_or(0);
6818            host::reject_promise_val(id, arg0(&args));
6819            Ok(Value::Undef)
6820        }
6821        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6822        // index so calling it twice is the spec's no-op rather than a re-tear.
6823        _ if name.starts_with("@@prevoke:") => {
6824            let i: u32 = name[10..].parse().unwrap_or(0);
6825            Ok(crate::proxy::revoke(i))
6826        }
6827        _ if name.starts_with("@@finpass:") => {
6828            // finally(cb) on fulfill: run cb, await whatever it returned, then
6829            // pass the original value through.
6830            let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6831            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6832            Ok(finally_chain(result, arg0(&args), false))
6833        }
6834        _ if name.starts_with("@@finthrow:") => {
6835            // finally(cb) on reject: same, then re-throw the original reason.
6836            let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6837            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6838            Ok(finally_chain(result, arg0(&args), true))
6839        }
6840        // The two thunks `finally_chain` hangs off that awaited promise. Each
6841        // carries the value it must reinstate in a one-slot cell, since a
6842        // builtin is identified only by its name and cannot close over one.
6843        _ if name.starts_with("@@finret:") => {
6844            let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6845            get_property(&Value::Obj(i), "0")
6846        }
6847        _ if name.starts_with("@@finrethrow:") => {
6848            let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6849            let reason = get_property(&Value::Obj(i), "0")?;
6850            with_host(|h| h.exc = Some(reason.clone()));
6851            Err(with_host(|h| error_string(h, &reason)))
6852        }
6853        _ => Err(host::type_error(&format!("{name} is not a function"))),
6854    }
6855}
6856
6857/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6858/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6859/// (matching Node's messages).
6860/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6861/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6862/// BigInt`. The old text said "value" literally, for every input.
6863fn bigint_convert_error(v: &Value) -> String {
6864    let shown = with_host(|h| h.str_of(v));
6865    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6866}
6867
6868/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6869/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6870/// BigInt arithmetic's operand check.
6871///
6872/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6873/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6874/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6875/// the type table — booleans convert (`true` → `1n`), strings parse with a
6876/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6877///
6878/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6879///
6880/// ```text
6881/// a[0] = true            → 1n
6882/// a[0] = '12'            → 12n
6883/// a[0] = []              → 0n        (ToPrimitive → "" → 0n)
6884/// a[0] = ['3']           → 3n
6885/// a[0] = 1               → TypeError: Cannot convert 1 to a BigInt
6886/// a[0] = new Number(3)   → TypeError: Cannot convert 3 to a BigInt
6887/// a[0] = 'a'             → SyntaxError: Cannot convert a to a BigInt
6888/// a[0] = {}              → SyntaxError: Cannot convert [object Object] to a BigInt
6889/// ```
6890pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6891    let prim = host::to_primitive(v, "number")?;
6892    if let Some(b) = with_host(|h| match h.get(&prim) {
6893        Some(JsObj::BigInt(b)) => Some(b.clone()),
6894        _ => None,
6895    }) {
6896        return Ok(b);
6897    }
6898    match &prim {
6899        Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6900        Value::Str(s) => host::parse_bigint_str(s)
6901            .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6902        _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6903            let s = with_host(|h| h.str_of(&prim));
6904            host::parse_bigint_str(&s)
6905                .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6906        }
6907        _ => Err(bigint_convert_error(&prim)),
6908    }
6909}
6910
6911fn bigint_ctor(v: &Value) -> Result<Value, String> {
6912    use num_bigint::BigInt;
6913    let big = match v {
6914        Value::Bool(b) => BigInt::from(*b as i64),
6915        Value::Int(n) => BigInt::from(*n),
6916        Value::Float(f) => {
6917            if !f.is_finite() || f.fract() != 0.0 {
6918                let disp = with_host(|h| h.str_of(v));
6919                return Err(format!(
6920                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6921                ));
6922            }
6923            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6924            // .toString` switches to exponential notation at 1e21, and
6925            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6926            // threw `Cannot convert value to a BigInt` where node returns
6927            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6928            // value, which is also what node reports for a magnitude past the
6929            // exactly-representable range (`BigInt(1e30)` is
6930            // `1000000000000000019884624838656n` in both).
6931            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6932                Some(b) => b,
6933                None => return Err(bigint_convert_error(v)),
6934            }
6935        }
6936        Value::Str(s) => match host::parse_bigint_str(s) {
6937            Some(b) => b,
6938            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6939        },
6940        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6941            Some(JsObj::BigInt(b)) => b,
6942            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6943                Some(b) => b,
6944                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6945            },
6946            _ => return Err(bigint_convert_error(v)),
6947        },
6948        _ => return Err(bigint_convert_error(v)),
6949    };
6950    Ok(with_host(|h| h.new_bigint(big)))
6951}
6952
6953/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6954/// its source (and flags, unless new ones are given).
6955fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6956    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6957        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6958        _ => {
6959            let a0 = arg0(args);
6960            // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6961            // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6962            // pattern instead of throwing.
6963            let src = if matches!(a0, Value::Undef) {
6964                String::new()
6965            } else {
6966                arg_to_string(args, 0)?
6967            };
6968            (src, None)
6969        }
6970    };
6971    let flags = match args.get(1) {
6972        Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6973        _ => existing_flags.unwrap_or_default(),
6974    };
6975    // An empty source compiles as the JS canonical `(?:)`.
6976    let src = if source.is_empty() {
6977        "(?:)".to_string()
6978    } else {
6979        source
6980    };
6981    crate::regexp::build_regexp(&src, &flags)
6982}
6983
6984/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
6985/// two's-complement (signed) or unsigned integer.
6986fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
6987    use num_bigint::BigInt;
6988    use num_traits::Signed;
6989    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
6990    if bits < 0 {
6991        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
6992    }
6993    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
6994        Some(b) => b,
6995        None => return Err(host::type_error("Cannot convert to a BigInt")),
6996    };
6997    let bits = bits as u32;
6998    if bits == 0 {
6999        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7000    }
7001    let modulus = BigInt::from(1) << bits; // 2^bits
7002                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7003    let mut r = &x % &modulus;
7004    if r.is_negative() {
7005        r += &modulus;
7006    }
7007    if !unsigned {
7008        let half = BigInt::from(1) << (bits - 1);
7009        if r >= half {
7010            r -= &modulus;
7011        }
7012    }
7013    Ok(with_host(|h| h.new_bigint(r)))
7014}
7015
7016/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7017/// interleaved with the substitutions.
7018fn string_raw(args: &[Value]) -> Result<Value, String> {
7019    let call_site = arg0(args);
7020    let raw = get_property(&call_site, "raw")?;
7021    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7022    let mut out = String::new();
7023    for (i, r) in raws.iter().enumerate() {
7024        out.push_str(&with_host(|h| h.str_of(r)));
7025        if i + 1 < raws.len() {
7026            if let Some(sub) = args.get(i + 1) {
7027                out.push_str(&with_host(|h| h.str_of(sub)));
7028            }
7029        }
7030    }
7031    Ok(with_host(|h| h.new_str(out)))
7032}
7033
7034/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7035/// fresh object; objects pass through.
7036/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7037/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7038/// ordinary object that also has internal slots, so it can carry own properties
7039/// like anything else — but its heap variant holds only those slots, so a write
7040/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7041pub fn uses_side_table(v: &Value) -> bool {
7042    matches!(
7043        with_host(|h| h.kind_of(v)),
7044        Some(
7045            ObjKind::Map
7046                | ObjKind::Set
7047                | ObjKind::Promise
7048                | ObjKind::RegExp
7049                | ObjKind::Generator
7050                | ObjKind::Symbol
7051                | ObjKind::BigInt
7052                | ObjKind::Iter
7053        )
7054    )
7055}
7056
7057fn object_call(args: Vec<Value>) -> Value {
7058    let a = arg0(&args);
7059    // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7060    // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7061    if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7062        return with_host(|h| h.new_object(IndexMap::new()));
7063    }
7064    to_object(&a)
7065}
7066
7067/// The name of the wrapper a primitive boxes into, or `None` when the value is
7068/// already an object.
7069fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7070    match v {
7071        Value::Int(_) | Value::Float(_) => Some("Number"),
7072        Value::Bool(_) => Some("Boolean"),
7073        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7074            Some(JsObj::Str(_)) => Some("String"),
7075            Some(JsObj::Symbol { .. }) => Some("Symbol"),
7076            Some(JsObj::BigInt(_)) => Some("BigInt"),
7077            _ => None,
7078        },
7079        _ => None,
7080    }
7081}
7082
7083/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7084/// for every other value. The slot is a hidden `@@primitive` own property —
7085/// the same `@@` marker convention the engine already uses for internal state,
7086/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7087pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7088    with_host(|h| match h.get(v) {
7089        Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7090        _ => None,
7091    })
7092}
7093
7094/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7095/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7096///
7097/// A String wrapper also owns its index properties and `length`, which is what
7098/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7099/// non-writable and non-configurable, as the exotic `String` object's are.
7100pub fn to_object(v: &Value) -> Value {
7101    let Some(ctor) = wrapper_ctor_of(v) else {
7102        return v.clone();
7103    };
7104    with_host(|h| h.ensure_wrapper_protos());
7105    let chars: Vec<String> = if ctor == "String" {
7106        with_host(|h| h.str_of(v))
7107            .chars()
7108            .map(|c| c.to_string())
7109            .collect()
7110    } else {
7111        Vec::new()
7112    };
7113    with_host(|h| {
7114        let mut m: IndexMap<String, Value> = IndexMap::new();
7115        for (i, c) in chars.iter().enumerate() {
7116            let s = h.new_str(c.clone());
7117            m.insert(i.to_string(), s);
7118        }
7119        let w = h.new_object(m);
7120        if ctor == "String" {
7121            for i in 0..chars.len() {
7122                h.set_prop_attrs(
7123                    &w,
7124                    &i.to_string(),
7125                    host::PropAttrs {
7126                        writable: false,
7127                        enumerable: true,
7128                        configurable: false,
7129                    },
7130                );
7131            }
7132            let len = Value::Float(chars.len() as f64);
7133            if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7134                p.insert("length".into(), len);
7135            }
7136            h.set_prop_attrs(
7137                &w,
7138                "length",
7139                host::PropAttrs {
7140                    writable: false,
7141                    enumerable: false,
7142                    configurable: false,
7143                },
7144            );
7145        }
7146        if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7147            p.insert("@@primitive".into(), v.clone());
7148        }
7149        if let Some(proto) = h.native_proto(ctor) {
7150            h.set_proto(&w, proto);
7151        }
7152        w
7153    })
7154}
7155
7156/// Construct via `new` for the builtin constructors.
7157pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7158    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7159    if let Some(r) = crate::stdlib::construct(name, &args) {
7160        return r;
7161    }
7162    match name {
7163        "Array" => {
7164            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7165            // A single NUMBER argument is a length and is validated as one
7166            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7167            // `new Array(2**32)` are all `RangeError: Invalid array length` on
7168            // node v26.7.0; only a non-number single argument is an element.
7169            if args.len() == 1 {
7170                if let Value::Float(_) | Value::Int(_) = args[0] {
7171                    let n = host::to_array_length(&args[0])?;
7172                    // Every element of `new Array(n)` is a HOLE, not a stored
7173                    // `undefined`: `Object.keys(Array(3))` is `[]`.
7174                    return Ok(with_host(|h| {
7175                        let a = h.new_array(vec![Value::Undef; n]);
7176                        h.mark_hole_range(&a, 0..n);
7177                        a
7178                    }));
7179                }
7180            }
7181            Ok(with_host(|h| h.new_array(args)))
7182        }
7183        "Object" => Ok(object_call(args)),
7184        // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7185        // form. These were not constructors at all, so every one threw.
7186        "String" => Ok(to_object(&host::to_string_value(
7187            &args
7188                .first()
7189                .cloned()
7190                .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7191        )?)),
7192        "Number" => Ok(to_object(&Value::Float(match args.first() {
7193            Some(a) => host::to_number_value(a)?,
7194            None => 0.0,
7195        }))),
7196        "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7197            h.truthy(&arg0(&args))
7198        })))),
7199        "Map" | "WeakMap" => {
7200            let weak = name == "WeakMap";
7201            let m = with_host(|h| {
7202                h.alloc(JsObj::Map {
7203                    entries: indexmap::IndexMap::new(),
7204                    weak,
7205                })
7206            });
7207            if let Some(init) = args
7208                .first()
7209                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7210            {
7211                // Stepped, not drained: an entry that is not a pair has to
7212                // stop the construction at that element and CLOSE the iterator
7213                // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7214                // infinite source was never reached and the constructor HUNG.
7215                host::iter_for_each(init, |p, _| {
7216                    // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7217                    // is iterable, so without this check `new Map(["ab"])`
7218                    // happily stored `'a' => 'b'` instead of throwing — and over
7219                    // an infinite source it never stopped.
7220                    if !with_host(|h| is_object_like(h, &p)) {
7221                        let shown = with_host(|h| h.str_of(&p));
7222                        return Err(host::type_error(&format!(
7223                            "Iterator value {shown} is not an entry object"
7224                        )));
7225                    }
7226                    // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7227                    // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7228                    // gives `undefined => undefined`, and an array-LIKE entry
7229                    // works. Iterating it instead accepted a string as a pair
7230                    // and rejected the array-like.
7231                    let k = get_property(&p, "0")?;
7232                    let v = get_property(&p, "1")?;
7233                    map_method(&m, "set", vec![k, v])?;
7234                    Ok(())
7235                })?;
7236            }
7237            Ok(m)
7238        }
7239        "Set" | "WeakSet" => {
7240            let weak = name == "WeakSet";
7241            let s = with_host(|h| {
7242                h.alloc(JsObj::Set {
7243                    entries: indexmap::IndexMap::new(),
7244                    weak,
7245                })
7246            });
7247            if let Some(init) = args
7248                .first()
7249                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7250            {
7251                host::iter_for_each(init, |v, _| {
7252                    set_method(&s, "add", vec![v])?;
7253                    Ok(())
7254                })?;
7255            }
7256            Ok(s)
7257        }
7258        "Promise" => new_promise(arg0(&args)),
7259        "Proxy" => crate::proxy::create(&args),
7260        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7261        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7262        // deprecation wrapper this way, so `require('body-parser')` — and with it
7263        // `require('express')` — dies at load without it.
7264        "Function" => function_ctor(&args),
7265        "RegExp" => regexp_ctor(&args),
7266        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7267        "Error" => make_error_checked(name, &args),
7268        // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7269        // legacy numeric `code` follows from it.
7270        "DOMException" => Ok(dom_exception(&args)),
7271        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7272        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7273    }
7274}
7275
7276/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7277/// outside the table — including the default `"Error"` — reports 0.
7278pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7279    ("IndexSizeError", 1.0),
7280    ("DOMStringSizeError", 2.0),
7281    ("HierarchyRequestError", 3.0),
7282    ("WrongDocumentError", 4.0),
7283    ("InvalidCharacterError", 5.0),
7284    ("NoDataAllowedError", 6.0),
7285    ("NoModificationAllowedError", 7.0),
7286    ("NotFoundError", 8.0),
7287    ("NotSupportedError", 9.0),
7288    ("InUseAttributeError", 10.0),
7289    ("InvalidStateError", 11.0),
7290    ("SyntaxError", 12.0),
7291    ("InvalidModificationError", 13.0),
7292    ("NamespaceError", 14.0),
7293    ("InvalidAccessError", 15.0),
7294    ("ValidationError", 16.0),
7295    ("TypeMismatchError", 17.0),
7296    ("SecurityError", 18.0),
7297    ("NetworkError", 19.0),
7298    ("AbortError", 20.0),
7299    ("URLMismatchError", 21.0),
7300    ("QuotaExceededError", 22.0),
7301    ("TimeoutError", 23.0),
7302    ("InvalidNodeTypeError", 24.0),
7303    ("DataCloneError", 25.0),
7304];
7305
7306/// The static name a `DOMException` code is exposed under: the error name minus
7307/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7308/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7309fn legacy_code_name(error_name: &str) -> String {
7310    let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7311    let mut out = String::new();
7312    for (i, c) in stem.chars().enumerate() {
7313        if c.is_ascii_uppercase() && i > 0 {
7314            out.push('_');
7315        }
7316        out.push(c.to_ascii_uppercase());
7317    }
7318    out.push_str("_ERR");
7319    out
7320}
7321
7322/// `new DOMException(message, name)`.
7323///
7324/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7325/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7326/// legacy number that name maps to.
7327pub fn dom_exception(args: &[Value]) -> Value {
7328    let message = match args.first() {
7329        None | Some(Value::Undef) => String::new(),
7330        Some(v) => with_host(|h| h.str_of(v)),
7331    };
7332    let name = match args.get(1) {
7333        None | Some(Value::Undef) => "Error".to_string(),
7334        Some(v) => with_host(|h| h.str_of(v)),
7335    };
7336    with_host(|h| dom_exception_with(h, &name, &message))
7337}
7338
7339/// `dom_exception` for a caller that already holds the host borrow.
7340pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7341    let name = name.to_string();
7342    let message = message.to_string();
7343    let code = DOM_EXCEPTION_CODES
7344        .iter()
7345        .find(|(n, _)| *n == name)
7346        .map(|(_, c)| *c)
7347        .unwrap_or(0.0);
7348    let head = if message.is_empty() {
7349        name.clone()
7350    } else {
7351        format!("{name}: {message}")
7352    };
7353    let e = synth_error(h, &head);
7354    {
7355        let nv = h.new_str(name);
7356        let mv = h.new_str(message);
7357        let sv = h.new_str(head);
7358        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7359            // `name`, `message` and `code` are PROTOTYPE accessors over internal
7360            // slots in node, so `stack` is the instance's only own property.
7361            // Storing them as own keys would show up in
7362            // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7363            p.shift_remove("message");
7364            p.insert("@@domName".into(), nv);
7365            p.insert("@@domMessage".into(), mv);
7366            p.insert("@@domCode".into(), Value::Float(code));
7367            p.insert("stack".into(), sv);
7368        }
7369        h.ensure_error_protos();
7370        if let Some(proto) = host::error_proto_of(h, "DOMException") {
7371            h.set_proto(&e, proto);
7372        }
7373    }
7374    e
7375}
7376
7377/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7378/// rather than as own properties. `None` for anything else.
7379pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7380    let slot = match name {
7381        "name" => "@@domName",
7382        "message" => "@@domMessage",
7383        "code" => "@@domCode",
7384        _ => return None,
7385    };
7386    with_host(|h| match h.get(recv) {
7387        Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7388        _ => None,
7389    })
7390}
7391
7392/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7393/// throw a value with extra own properties on it.
7394pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7395    let m = with_host(|h| h.new_str(msg.to_string()));
7396    make_error_inner(name, &[m])
7397}
7398
7399/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7400/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7401/// rendered `Symbol(desc)` into `.message`.
7402fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7403    if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7404        // AggregateError's message is its SECOND argument.
7405        let idx = usize::from(name == "AggregateError");
7406        if idx == 0 {
7407            host::to_string_value(m)?;
7408        } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7409            host::to_string_value(m2)?;
7410        }
7411    }
7412    Ok(make_error_inner(name, args))
7413}
7414
7415fn make_error_inner(name: &str, args: &[Value]) -> Value {
7416    // `new AggregateError(errors, message)` takes the causes FIRST; every other
7417    // error constructor takes the message first.
7418    let agg = name == "AggregateError";
7419    let (errors, args) = if agg {
7420        (
7421            Some(args.first().cloned().unwrap_or(Value::Undef)),
7422            args.get(1..).unwrap_or(&[]),
7423        )
7424    } else {
7425        (None, args)
7426    };
7427    with_host(|h| {
7428        h.ensure_error_protos();
7429        let mut props: IndexMap<String, Value> = IndexMap::new();
7430        let msg = args
7431            .first()
7432            .filter(|a| !matches!(a, Value::Undef))
7433            .map(|a| h.str_of(a));
7434        if let Some(m) = &msg {
7435            let mv = h.new_str(m.clone());
7436            props.insert("message".into(), mv);
7437        }
7438        // `.stack` is engine-specific; a simple `Name: message` header line
7439        // suffices for parity (the fuzzer never prints raw stacks).
7440        //
7441        // V8 formats that header LAZILY, on the first read, from whatever `name`
7442        // and `message` the error carries at that moment — which is why the
7443        // near-universal
7444        //
7445        //     class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7446        //
7447        // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7448        // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7449        // carries the frames so the read can redo it; see `materialize_stack`.
7450        let frames = h.stack_frames();
7451        let stack = match &msg {
7452            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7453            _ => format!("{name}{frames}"),
7454        };
7455        let sv = h.new_str(stack);
7456        props.insert("stack".into(), sv);
7457        let raw = h.new_str(frames);
7458        props.insert("@@stackRaw".into(), raw);
7459        if let Some(errs) = errors {
7460            // Materialize the iterable into the own `errors` array property.
7461            let items = h.iter_vec(&errs).unwrap_or_default();
7462            let arr = h.new_array(items);
7463            props.insert("errors".into(), arr);
7464        }
7465        // `new Error(msg, { cause })` (ES2022): installed only when the options
7466        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7467        let opts = args.get(1);
7468        if let Some(cause) = opts.and_then(|o| match h.get(o) {
7469            Some(JsObj::Object(p)) => p.get("cause").cloned(),
7470            _ => None,
7471        }) {
7472            props.insert("cause".into(), cause);
7473        }
7474        let e = h.new_object(props);
7475        if let Some(p) = host::error_proto_of(h, name) {
7476            h.set_proto(&e, p);
7477        }
7478        // Every own slot an error constructor installs is non-enumerable in V8,
7479        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7480        // `{}` — properties a *script* later assigns stay enumerable.
7481        for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7482            h.hide_prop(&e, k);
7483        }
7484        e
7485    })
7486}
7487
7488fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7489    // Node's console.log(...args) === util.format(...args): printf-style
7490    // substitution when the first arg is a format string, else inspect-and-join.
7491    // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7492    // the caller instead of printing a line — so nothing is written on failure.
7493    let line: String = crate::stdlib::util::format(args)?;
7494    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7495    Ok(())
7496}
7497
7498fn arg0(args: &[Value]) -> Value {
7499    args.first().cloned().unwrap_or(Value::Undef)
7500}
7501/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7502/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7503/// instead rendered `Symbol(desc)` into its result and reported nothing.
7504fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7505    let v = args.get(i).cloned().unwrap_or(Value::Undef);
7506    let sv = host::to_string_value(&v)?;
7507    Ok(with_host(|h| h.str_of(&sv)))
7508}
7509
7510fn arg_num(args: &[Value], i: usize) -> f64 {
7511    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7512}
7513
7514fn is_integer(v: Value) -> bool {
7515    match v {
7516        Value::Int(_) => true,
7517        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7518        _ => false,
7519    }
7520}
7521fn is_safe_integer(v: Value) -> bool {
7522    match v {
7523        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7524        Value::Int(_) => true,
7525        _ => false,
7526    }
7527}
7528
7529/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7530/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7531/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7532fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7533    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7534    const UNRESERVED: &[u8] =
7535        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7536    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7537    const RESERVED: &[u8] = b";,/?:@&=+$#";
7538    let mut out = String::with_capacity(s.len());
7539    for &b in s.as_bytes() {
7540        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7541            out.push(b as char);
7542        } else {
7543            out.push('%');
7544            out.push(
7545                char::from_digit((b >> 4) as u32, 16)
7546                    .unwrap()
7547                    .to_ascii_uppercase(),
7548            );
7549            out.push(
7550                char::from_digit((b & 0xf) as u32, 16)
7551                    .unwrap()
7552                    .to_ascii_uppercase(),
7553            );
7554        }
7555    }
7556    Ok(with_host(|h| h.new_str(out)))
7557}
7558
7559/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7560/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7561/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7562fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7563    const RESERVED: &[u8] = b";,/?:@&=+$#";
7564    let bytes = s.as_bytes();
7565    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7566    let mut i = 0;
7567    while i < bytes.len() {
7568        if bytes[i] == b'%' {
7569            if i + 2 >= bytes.len() {
7570                return Err("URIError: URI malformed".into());
7571            }
7572            let hi = (bytes[i + 1] as char).to_digit(16);
7573            let lo = (bytes[i + 2] as char).to_digit(16);
7574            match (hi, lo) {
7575                (Some(h), Some(l)) => {
7576                    let byte = (h * 16 + l) as u8;
7577                    // decodeURI keeps reserved-delimiter escapes literal.
7578                    if uri && RESERVED.contains(&byte) {
7579                        out.extend_from_slice(&bytes[i..i + 3]);
7580                    } else {
7581                        out.push(byte);
7582                    }
7583                    i += 3;
7584                }
7585                _ => return Err("URIError: URI malformed".into()),
7586            }
7587        } else {
7588            out.push(bytes[i]);
7589            i += 1;
7590        }
7591    }
7592    match String::from_utf8(out) {
7593        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7594        Err(_) => Err("URIError: URI malformed".into()),
7595    }
7596}
7597
7598/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7599/// present in every engine and still reached by old libraries (jQuery's cookie
7600/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7601/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7602/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7603/// character yields the two escapes of its surrogate pair
7604/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7605///
7606/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7607/// it keeps `@*_+-./` and drops `!~'()`.
7608fn legacy_escape(s: &str) -> Result<Value, String> {
7609    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7610    let mut out = String::with_capacity(s.len());
7611    for u in s.encode_utf16() {
7612        if u < 0x100 {
7613            if KEEP.contains(&(u as u8)) {
7614                out.push(u as u8 as char);
7615            } else {
7616                out.push_str(&format!("%{u:02X}"));
7617            }
7618        } else {
7619            out.push_str(&format!("%u{u:04X}"));
7620        }
7621    }
7622    Ok(with_host(|h| h.new_str(out)))
7623}
7624
7625/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7626/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7627/// `%XX` or `%uXXXX` escape is passed through literally
7628/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7629///
7630/// Decoding is done in code-unit space and re-joined at the end so a
7631/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7632fn legacy_unescape(s: &str) -> Result<Value, String> {
7633    let b = s.as_bytes();
7634    let hex = |i: usize, n: usize| -> Option<u16> {
7635        if i + n > b.len() {
7636            return None;
7637        }
7638        let mut v: u16 = 0;
7639        for &c in &b[i..i + n] {
7640            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7641        }
7642        Some(v)
7643    };
7644    let units: Vec<u16> = s.encode_utf16().collect();
7645    let mut out: Vec<u16> = Vec::with_capacity(units.len());
7646    let mut i = 0;
7647    while i < b.len() {
7648        // Escapes are pure ASCII, so a byte index is a unit index up to here —
7649        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7650        if b[i] == b'%' {
7651            if let Some(u) = hex(i + 1, 2) {
7652                out.push(u);
7653                i += 3;
7654                continue;
7655            }
7656            if b.get(i + 1) == Some(&b'u') {
7657                if let Some(u) = hex(i + 2, 4) {
7658                    out.push(u);
7659                    i += 6;
7660                    continue;
7661                }
7662            }
7663        }
7664        let c = s[i..].chars().next().unwrap_or('%');
7665        let mut buf = [0u16; 2];
7666        out.extend_from_slice(c.encode_utf16(&mut buf));
7667        i += c.len_utf8();
7668    }
7669    Ok(with_host(|h| {
7670        h.new_str(crate::utf16::to_string_lossy(&out))
7671    }))
7672}
7673
7674/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7675/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7676/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7677/// instead of converting it, which both swallowed that throw and ignored any
7678/// `toString` the value defines.
7679fn parse_int(args: &[Value]) -> Result<f64, String> {
7680    // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7681    let sv = host::to_string_value(&arg0(args))?;
7682    // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7683    // infallible read below does no `ToPrimitive`, so an object radix came out
7684    // as NaN and the parse silently fell back to auto-detection.
7685    let radix = match args.get(1) {
7686        Some(r) if !matches!(r, Value::Undef) => {
7687            vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7688        }
7689        _ => args.to_vec(),
7690    };
7691    Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7692}
7693
7694fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7695    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7696    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7697    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7698    let radix_arg = args
7699        .get(1)
7700        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7701    let radix = match radix_arg {
7702        Some(0) | None => None,
7703        Some(r) if (2..=36).contains(&r) => Some(r as u32),
7704        Some(_) => return f64::NAN,
7705    };
7706    let t = crate::utf16::js_trim_start(s);
7707    let (neg, digits) = match t.strip_prefix('-') {
7708        Some(rest) => (true, rest),
7709        None => (false, t.strip_prefix('+').unwrap_or(t)),
7710    };
7711    let (radix, digits) = match radix {
7712        Some(16) => (
7713            16u32,
7714            digits
7715                .strip_prefix("0x")
7716                .or_else(|| digits.strip_prefix("0X"))
7717                .unwrap_or(digits),
7718        ),
7719        Some(r) => (r, digits),
7720        None => {
7721            if let Some(hex) = digits
7722                .strip_prefix("0x")
7723                .or_else(|| digits.strip_prefix("0X"))
7724            {
7725                (16, hex)
7726            } else {
7727                (10, digits)
7728            }
7729        }
7730    };
7731    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7732    if valid.is_empty() {
7733        return f64::NAN;
7734    }
7735    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7736    // digits and the error was mapped to `NaN`, so
7737    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7738    // asks for the mathematical value rounded to a Number, which is what
7739    // repeated multiply-accumulate in `f64` produces.
7740    let n = if radix == 10 {
7741        // Rust's decimal float parser is correctly rounded; digit-by-digit
7742        // multiply-accumulate is not, and drifted a ULP on long inputs
7743        // (`parseInt("999999999999999999999999")` came out
7744        // 1.0000000000000003e+24 rather than 1e+24).
7745        valid.parse::<f64>().unwrap_or(f64::NAN)
7746    } else {
7747        let mut n = 0.0f64;
7748        for c in valid.chars() {
7749            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7750        }
7751        n
7752    };
7753    if neg {
7754        -n
7755    } else {
7756        n
7757    }
7758}
7759
7760/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7761fn parse_float(args: &[Value]) -> Result<f64, String> {
7762    let sv = host::to_string_value(&arg0(args))?;
7763    Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7764}
7765
7766fn parse_float_str(s: &str) -> f64 {
7767    let t = crate::utf16::js_trim_start(s);
7768    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7769    let inf_body = t
7770        .strip_prefix('+')
7771        .or_else(|| t.strip_prefix('-'))
7772        .unwrap_or(t);
7773    if inf_body.starts_with("Infinity") {
7774        return if t.starts_with('-') {
7775            f64::NEG_INFINITY
7776        } else {
7777            f64::INFINITY
7778        };
7779    }
7780    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7781    // not the same as the longest run of characters that could appear in one:
7782    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7783    // only valid once a digit follows `e`. Tracking `end` at every character
7784    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7785    // came back NaN.
7786    let mut end = 0;
7787    let bytes = t.as_bytes();
7788    let mut seen_dot = false;
7789    let mut seen_e = false;
7790    let mut digits_before_dot = false;
7791    for (i, &c) in bytes.iter().enumerate() {
7792        match c {
7793            b'0'..=b'9' => {
7794                if !seen_dot && !seen_e {
7795                    digits_before_dot = true;
7796                }
7797                end = i + 1;
7798            }
7799            // A sign is only meaningful leading, or straight after the exponent
7800            // marker; it never completes a literal on its own.
7801            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7802            // `1.` is a complete literal; a bare `.` is not.
7803            b'.' if !seen_dot && !seen_e => {
7804                seen_dot = true;
7805                if digits_before_dot {
7806                    end = i + 1;
7807                }
7808            }
7809            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7810            _ => break,
7811        }
7812    }
7813    if end == 0 {
7814        return f64::NAN;
7815    }
7816    t[..end].parse::<f64>().unwrap_or(f64::NAN)
7817}
7818
7819/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7820/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7821/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7822/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7823/// rather than 1.
7824pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7825    if exp == 0.0 {
7826        return 1.0;
7827    }
7828    if base.is_nan() || exp.is_nan() {
7829        return f64::NAN;
7830    }
7831    if base.abs() == 1.0 && exp.is_infinite() {
7832        return f64::NAN;
7833    }
7834    base.powf(exp)
7835}
7836
7837fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7838    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7839    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7840    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7841    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7842    // exception: it never reads an argument, so `Math.random(1n)` is fine.
7843    // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7844    // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7845    // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7846    // the host itself and cannot run inside another borrow.
7847    let is_bigint = |a: &Value| {
7848        if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7849            return true;
7850        }
7851        match wrapped_primitive(a) {
7852            Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7853            None => false,
7854        }
7855    };
7856    if fname != "random" && args.iter().any(is_bigint) {
7857        return Err(host::type_error(
7858            "Cannot convert a BigInt value to a number",
7859        ));
7860    }
7861    // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7862    // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7863    // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7864    // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7865    // variadic, and coercing only the first few silently DROPPED the rest —
7866    // `Math.max(...gen)` over five values answered for four of them.
7867    let mut coerced = Vec::with_capacity(args.len());
7868    for a in args {
7869        if matches!(a, Value::Undef) {
7870            coerced.push(a.clone());
7871            continue;
7872        }
7873        let p = host::to_primitive(a, "number")?;
7874        coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7875    }
7876    let args: &[Value] = &coerced;
7877    let x = arg_num(args, 0);
7878    let r = match fname {
7879        "floor" => x.floor(),
7880        "ceil" => x.ceil(),
7881        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7882        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7883        // before the floor sees it, so it answers 1 for the largest double below
7884        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7885        // perturbs integers above 2^52, where `x + 0.5` is no longer
7886        // representable (`Math.round(4503599627370497)` must be the input).
7887        // Splitting the zero-band cases out first also carries the signed zero
7888        // the spec asks for without a post-hoc patch.
7889        "round" => {
7890            if !x.is_finite() || x == 0.0 {
7891                x
7892            } else if x > 0.0 && x < 0.5 {
7893                0.0
7894            } else if (-0.5..0.0).contains(&x) {
7895                -0.0
7896            } else {
7897                // |x| >= 0.5, so `floor` and the subtraction are both exact
7898                // (every double >= 2^52 is already an integer and yields 0 here).
7899                let f = x.floor();
7900                if x - f >= 0.5 {
7901                    f + 1.0
7902                } else {
7903                    f
7904                }
7905            }
7906        }
7907        "trunc" => x.trunc(),
7908        "abs" => x.abs(),
7909        "sign" => {
7910            if x.is_nan() {
7911                f64::NAN
7912            } else if x > 0.0 {
7913                1.0
7914            } else if x < 0.0 {
7915                -1.0
7916            } else {
7917                x
7918            }
7919        }
7920        "sqrt" => x.sqrt(),
7921        "cbrt" => x.cbrt(),
7922        "exp" => x.exp(),
7923        "log" => x.ln(),
7924        "log2" => x.log2(),
7925        "log10" => x.log10(),
7926        "sin" => x.sin(),
7927        "cos" => x.cos(),
7928        "tan" => x.tan(),
7929        "asin" => x.asin(),
7930        "acos" => x.acos(),
7931        "atan" => x.atan(),
7932        "atan2" => x.atan2(arg_num(args, 1)),
7933        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7934        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7935        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7936        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7937        // clause is shared.
7938        "pow" => js_pow(x, arg_num(args, 1)),
7939        // Hyperbolics and the two precision-preserving log/exp forms.
7940        "sinh" => x.sinh(),
7941        "cosh" => x.cosh(),
7942        "tanh" => x.tanh(),
7943        "asinh" => x.asinh(),
7944        "acosh" => x.acosh(),
7945        "atanh" => x.atanh(),
7946        "log1p" => x.ln_1p(),
7947        "expm1" => x.exp_m1(),
7948        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7949        // wraparound, reinterpret as a signed 32-bit result.
7950        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7951        "hypot" => {
7952            // Scale by the largest magnitude before squaring — this avoids the
7953            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7954            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7955            let mut max = 0.0f64;
7956            for x in &xs {
7957                if x.abs() > max {
7958                    max = x.abs();
7959                }
7960            }
7961            if xs.iter().any(|x| x.is_infinite()) {
7962                f64::INFINITY
7963            } else if max == 0.0 || !max.is_finite() {
7964                max
7965            } else {
7966                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7967                max * s.sqrt()
7968            }
7969        }
7970        "random" => pseudo_random(),
7971        "max" => {
7972            if args.is_empty() {
7973                f64::NEG_INFINITY
7974            } else {
7975                let mut m = f64::NEG_INFINITY;
7976                for a in args {
7977                    let n = with_host(|h| h.to_number(a));
7978                    if n.is_nan() {
7979                        return Ok(Value::Float(f64::NAN));
7980                    }
7981                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7982                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
7983                    // must not keep the -0 the first iteration installed.
7984                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
7985                        m = n;
7986                    }
7987                }
7988                m
7989            }
7990        }
7991        "min" => {
7992            if args.is_empty() {
7993                f64::INFINITY
7994            } else {
7995                let mut m = f64::INFINITY;
7996                for a in args {
7997                    let n = with_host(|h| h.to_number(a));
7998                    if n.is_nan() {
7999                        return Ok(Value::Float(f64::NAN));
8000                    }
8001                    // Mirror of `max`: -0 ranks below +0 even though `<` says
8002                    // they are equal, so `Math.min(0, -0)` is -0.
8003                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8004                        m = n;
8005                    }
8006                }
8007                m
8008            }
8009        }
8010        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8011        "clz32" => {
8012            let u = if x.is_finite() {
8013                x.trunc().rem_euclid(4294967296.0) as u32
8014            } else {
8015                0
8016            };
8017            u.leading_zeros() as f64
8018        }
8019        // Round to the nearest single-precision float.
8020        "fround" => (x as f32) as f64,
8021        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8022    };
8023    Ok(Value::Float(r))
8024}
8025
8026/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8027/// Node by nature; kept simple).
8028fn pseudo_random() -> f64 {
8029    use std::cell::Cell;
8030    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8031    SEED.with(|s| {
8032        let mut x = s.get();
8033        x ^= x << 13;
8034        x ^= x >> 7;
8035        x ^= x << 17;
8036        s.set(x);
8037        (x >> 11) as f64 / (1u64 << 53) as f64
8038    })
8039}
8040
8041// ── Object.* ──────────────────────────────────────────────────────────────────
8042
8043/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8044/// properties (10.4.3 `StringExoticObject`).
8045///
8046/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8047/// an exotic object whose own keys are its code-unit indices plus `length`;
8048/// this is the descriptor half of that. (The KEY half lives in
8049/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8050/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8051///
8052/// A boxed `String` object is deliberately NOT routed here: it can carry
8053/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8054/// existing path already reports them alongside the indices.
8055fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8056    // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8057    // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8058    // never catches one.
8059    let s = match v {
8060        Value::Str(s) => (**s).clone(),
8061        _ => with_host(|h| match h.get(v) {
8062            Some(JsObj::Str(s)) => Some(s.clone()),
8063            _ => None,
8064        })?,
8065    };
8066    let units = crate::utf16::Units::of(&s);
8067    Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8068}
8069
8070fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8071    let v = arg0(&args);
8072    require_object_coercible(&v)?;
8073    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8074    // reports every own STRING key the trap named; the enumerating modes
8075    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8076    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8077        if mode == 3 {
8078            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8079            return Ok(with_host(|h| {
8080                let out: Vec<Value> = keys
8081                    .into_iter()
8082                    .filter(|k| !host::is_symbol_key(k))
8083                    .map(|k| h.new_str(k))
8084                    .collect();
8085                h.new_array(out)
8086            }));
8087        }
8088        // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8089        // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8090        // Going through `own_enum_entries` fired the `get` trap once per key, so
8091        // the observable trap sequence carried a trailing `get` node does not
8092        // emit, and a trap with side effects ran when it should not have.
8093        if mode == 0 {
8094            let keys = crate::proxy::own_enum_string_keys(&v)?;
8095            return Ok(with_host(|h| {
8096                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8097                h.new_array(out)
8098            }));
8099        }
8100        let entries = crate::proxy::own_enum_entries(&v)?;
8101        return Ok(with_host(|h| {
8102            let out: Vec<Value> = entries
8103                .into_iter()
8104                .map(|(k, val)| match mode {
8105                    0 => h.new_str(k),
8106                    1 => val,
8107                    _ => {
8108                        let ks = h.new_str(k);
8109                        h.new_array(vec![ks, val])
8110                    }
8111                })
8112                .collect();
8113            h.new_array(out)
8114        }));
8115    }
8116    // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8117    // .prototype`, `String.prototype`, the error hierarchy — answers from the
8118    // generated table too. It was answering from its own property map instead,
8119    // which carries neither the right names nor V8's order: `Symbol.prototype`
8120    // reported `toLocaleString` and omitted `description`, and
8121    // `String.prototype` omitted `length` and every Annex B HTML method.
8122    //
8123    // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8124    // the two representations of a prototype cannot answer differently.
8125    let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8126        .filter(|ns| intrinsic_proto_members(ns).is_some());
8127    // A builtin prototype namespace that exposes enumerable methods for copying
8128    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8129    if let Some(ns) = real_proto_ns.or_else(|| {
8130        with_host(|h| match h.get(&v) {
8131            Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8132            _ => None,
8133        })
8134    }) {
8135        // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8136        // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8137        // interface, which the table records per name.
8138        if let Some(members) = intrinsic_proto_members(&ns) {
8139            let ctor = ns.trim_end_matches(".prototype");
8140            let mut names: Vec<String> = members
8141                .iter()
8142                .filter(|m| mode == 3 || m.starts_with('+'))
8143                .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8144                // `getOwnPropertyNames` reports STRING keys only; the table's
8145                // `@@` entries are symbol-keyed members and belong to
8146                // `getOwnPropertySymbols` instead.
8147                .filter(|m| !m.starts_with("@@"))
8148                .collect();
8149            // Plus whatever a script patched onto this prototype under a NEW
8150            // name — an ordinary enumerable own property, so it lists in every
8151            // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8152            // an assignment that `Array.prototype.patch` read back happily.
8153            //
8154            // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8155            // that member's attributes alone: restoring a saved `join` must not
8156            // turn it into an enumerable key.
8157            for k in with_host(|h| h.builtin_static_keys(&ns)) {
8158                if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8159                    names.push(k);
8160                }
8161            }
8162            return Ok(with_host(|h| {
8163                let out: Vec<Value> = names
8164                    .iter()
8165                    .map(|name| {
8166                        // An accessor member has no thunk — `Map.prototype.size`
8167                        // is not a function — so a VALUE read of one answers
8168                        // undefined rather than synthesizing a callable.
8169                        let val = |h: &mut host::JsHost| {
8170                            if let Some(v) = h.builtin_static(&ns, name) {
8171                                return v;
8172                            }
8173                            let key = format!("@proto:{ctor}:{name}");
8174                            if builtin_meta(&key).is_some() {
8175                                h.alloc(JsObj::Builtin(key))
8176                            } else {
8177                                Value::Undef
8178                            }
8179                        };
8180                        match mode {
8181                            1 => val(h),
8182                            2 => {
8183                                let ks = h.new_str(name.clone());
8184                                let v = val(h);
8185                                h.new_array(vec![ks, v])
8186                            }
8187                            _ => h.new_str(name.clone()),
8188                        }
8189                    })
8190                    .collect();
8191                h.new_array(out)
8192            }));
8193        }
8194        if let Some(names) = builtin_proto_method_names(&ns) {
8195            return Ok(with_host(|h| {
8196                let out: Vec<Value> = names
8197                    .iter()
8198                    .map(|name| match mode {
8199                        1 => h.alloc(JsObj::Builtin(format!(
8200                            "@proto:{}:{name}",
8201                            ns.trim_end_matches(".prototype")
8202                        ))),
8203                        2 => {
8204                            let ks = h.new_str(*name);
8205                            let val = h.alloc(JsObj::Builtin(format!(
8206                                "@proto:{}:{name}",
8207                                ns.trim_end_matches(".prototype")
8208                            )));
8209                            h.new_array(vec![ks, val])
8210                        }
8211                        _ => h.new_str(*name),
8212                    })
8213                    .collect();
8214                h.new_array(out)
8215            }));
8216        }
8217        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8218        // keys are the members node-js implements, each resolved to the same
8219        // first-class value a property read would give.
8220        let mut names = crate::stdlib::namespace_keys(&ns);
8221        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8222        // its members live in the builtin dispatch table. They are
8223        // non-enumerable in V8, so they surface only under
8224        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8225        if names.is_empty() && mode == 3 {
8226            let prefix = format!("{ns}.");
8227            // A builtin constructor's own `length`/`name`/`prototype` come
8228            // first, as they do in V8.
8229            if is_builtin_ctor(&ns) {
8230                names.extend(["length", "name", "prototype"].map(str::to_string));
8231            }
8232            names.extend(
8233                NS_METHODS
8234                    .iter()
8235                    .filter_map(|q| q.strip_prefix(&prefix))
8236                    .map(|m| m.to_string()),
8237            );
8238            // The numeric constants are members too. Without them
8239            // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8240            // actually answers — the eight it dropped being `PI` and its
8241            // siblings, which read fine and now own a descriptor as well.
8242            names.extend(
8243                namespace_constants(&ns)
8244                    .iter()
8245                    .map(|(k, _)| (*k).to_string()),
8246            );
8247        }
8248        // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8249        // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8250        // was `[]`, which said the function had no properties at all while both
8251        // of them read back a value. `length` is listed only where the intrinsic
8252        // table has an arity, so the names never advertise a read that answers
8253        // `undefined`.
8254        if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8255            if builtin_meta(&ns).is_some() {
8256                names.push("length".to_string());
8257            }
8258            names.push("name".to_string());
8259        }
8260        // Whatever a script assigned onto the namespace, in assignment order and
8261        // after the built-in members — an ordinary enumerable own property, so
8262        // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8263        // missing from every listing, which made a patched prototype read as
8264        // unpatched to any code that enumerates rather than reads.
8265        for k in with_host(|h| h.builtin_static_keys(&ns)) {
8266            if !names.contains(&k) {
8267                names.push(k);
8268            }
8269        }
8270        if !names.is_empty() {
8271            let entries: Vec<(String, Value)> = names
8272                .into_iter()
8273                .map(|k| {
8274                    let val = namespace_property(&ns, &k);
8275                    (k, val)
8276                })
8277                .collect();
8278            return Ok(with_host(|h| {
8279                let out: Vec<Value> = entries
8280                    .into_iter()
8281                    .map(|(k, val)| match mode {
8282                        1 => val,
8283                        2 => {
8284                            let ks = h.new_str(k);
8285                            h.new_array(vec![ks, val])
8286                        }
8287                        _ => h.new_str(k),
8288                    })
8289                    .collect();
8290                h.new_array(out)
8291            }));
8292        }
8293    }
8294    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8295    // non-enumerable ones, plus the exotic `length` an array carries.
8296    let entries: Vec<(String, Value)> = with_host(|h| {
8297        if mode == 3 {
8298            // An array's exotic `length` is already placed (after the indices,
8299            // before the ordinary string keys) by `own_key_names`.
8300            return h
8301                .own_key_names(&v, false)
8302                .into_iter()
8303                .map(|k| (k, Value::Undef))
8304                .collect();
8305        }
8306        Vec::new()
8307    });
8308    // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8309    // key/value pairs, so asking it for them ran every enumerable getter —
8310    // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8311    // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8312    // side effects:
8313    //
8314    //     let n = 0; const o = { get g() { n++; return 1 } };
8315    //     Object.keys(o); n   // was 1, node says 0
8316    //
8317    // `values`/`entries` (modes 1 and 2) do read, and still do.
8318    let entries = match mode {
8319        3 => entries,
8320        0 => with_host(|h| h.own_enum_key_names(&v))
8321            .into_iter()
8322            .map(|k| (k, Value::Undef))
8323            .collect(),
8324        _ => host::own_enum_entries_deep(&v)?,
8325    };
8326    Ok(with_host(|h| {
8327        let out: Vec<Value> = entries
8328            .into_iter()
8329            .map(|(k, val)| match mode {
8330                0 | 3 => h.new_str(k),
8331                1 => val,
8332                _ => {
8333                    let ks = h.new_str(k);
8334                    h.new_array(vec![ks, val])
8335                }
8336            })
8337            .collect();
8338        h.new_array(out)
8339    }))
8340}
8341
8342fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8343    let target = arg0(&args);
8344    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8345    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8346    require_object_coercible(&target)?;
8347    for src in args.iter().skip(1) {
8348        // `Object.assign` copies own *enumerable* properties, running any getter
8349        // — symbol-keyed ones included (7.3.25).
8350        let entries = host::own_enum_entries_deep(src)?;
8351        let syms = with_host(|h| h.own_symbol_entries(src));
8352        // A plain object target is filled in place (one borrow, then a single
8353        // re-canonicalization of the integer-index keys).
8354        let filled = with_host(|h| {
8355            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8356                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8357                    p.insert(k, v);
8358                }
8359                host::canonicalize_own_keys(p);
8360                return true;
8361            }
8362            false
8363        });
8364        // Any OTHER target — an array being the common one — goes through the
8365        // ordinary Set path. The in-place branch above matched `JsObj::Object`
8366        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8367        // returned the untouched array: no error, just a missing property. The
8368        // Set path is what an `arr.extra = 9` assignment already used, so index
8369        // and non-index keys land where they do for a direct write.
8370        if !filled {
8371            for (k, v) in entries.into_iter().chain(syms) {
8372                set_property(&target, &k, v)?;
8373            }
8374        }
8375    }
8376    Ok(target)
8377}
8378
8379fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8380    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8381    let mut props: IndexMap<String, Value> = IndexMap::new();
8382    for p in pairs {
8383        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8384        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8385        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8386        props.insert(key, val);
8387    }
8388    Ok(with_host(|h| h.new_object(props)))
8389}
8390
8391/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8392/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8393/// members mapped to that key, in first-seen key order.
8394fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8395    group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8396    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8397    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8398    // Stepped, not drained: the callback runs per element, so a throwing one
8399    // stops at the first. Draining first meant an infinite source never reached
8400    // the callback at all and the call HUNG.
8401    host::iter_for_each(&arg0(&args), |item, i| {
8402        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8403        let key = with_host(|h| h.property_key(&key_v));
8404        groups.entry(key).or_default().push(item);
8405        Ok(())
8406    })?;
8407    let props: IndexMap<String, Value> = with_host(|h| {
8408        groups
8409            .into_iter()
8410            .map(|(k, v)| (k, h.new_array(v)))
8411            .collect()
8412    });
8413    let obj = with_host(|h| h.new_object(props));
8414    // A null-prototype object (as Node returns), so it has no inherited members.
8415    with_host(|h| {
8416        let nv = h.null();
8417        h.set_proto(&obj, nv);
8418    });
8419    Ok(obj)
8420}
8421
8422/// The `groupBy` family words a non-iterable argument its OWN way — a third
8423/// vocabulary, alongside the array-literal spread's and the call spread's:
8424///
8425/// ```text
8426/// null / undefined   "<Name> called on null or undefined"
8427/// anything else      "<typeof> [value ]is not iterable (cannot read property
8428///                     Symbol(Symbol.iterator))"
8429/// ```
8430///
8431/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8432/// string and a boolean name the value too.
8433fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8434    if with_host(|h| h.is_nullish(v)) {
8435        return Err(host::type_error(&format!(
8436            "{name} called on null or undefined"
8437        )));
8438    }
8439    // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8440    // here, so the stepping loop below then saw an exhausted one — the finite
8441    // case returned an empty group and the infinite case was back to hanging.
8442    let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8443    if with_host(|h| host::is_callable(h, &iter_fn)) {
8444        return Ok(());
8445    }
8446    Err(host::type_error(&not_iterable_typed(v)))
8447}
8448
8449/// The `<type> <value> is not iterable (cannot read property
8450/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8451/// no name to report: a plain object, a symbol and a bigint name only their
8452/// TYPE; a number, a string and a boolean name the value too.
8453pub(crate) fn not_iterable_typed(v: &Value) -> String {
8454    let shown = with_host(|h| {
8455        let kind = h.type_of(v);
8456        match kind {
8457            "object" | "symbol" | "bigint" => kind.to_string(),
8458            "string" => format!("string \"{}\"", h.str_of(v)),
8459            _ => format!("{kind} {}", h.str_of(v)),
8460        }
8461    });
8462    format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8463}
8464
8465/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8466/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8467fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8468    group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8469    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8470    let m = with_host(|h| {
8471        h.alloc(JsObj::Map {
8472            entries: IndexMap::new(),
8473            weak: false,
8474        })
8475    });
8476    // Stepped for the same reason `Object.groupBy` is.
8477    host::iter_for_each(&arg0(&args), |item, i| {
8478        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8479        let existing = map_method(&m, "get", vec![key_v.clone()])?;
8480        if matches!(existing, Value::Undef) {
8481            let arr = with_host(|h| h.new_array(vec![item]));
8482            map_method(&m, "set", vec![key_v, arr])?;
8483        } else {
8484            with_host(|h| {
8485                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8486                    a.push(item);
8487                }
8488            });
8489        }
8490        Ok(())
8491    })?;
8492    Ok(m)
8493}
8494
8495/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8496/// element and each `mapFn` result.
8497///
8498/// Written in JavaScript and compiled once, because the operation IS an async
8499/// function: a Rust builtin runs outside any coroutine and has no way to await,
8500/// so draining a promise from there would mean running the microtask queue by
8501/// hand. Delegating to the engine's own `async`/`for await` keeps the
8502/// suspension semantics — and the ordering they imply — exactly the language's.
8503///
8504/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8505/// array-like. Everything iterable goes through `for await`, which awaits a sync
8506/// source's elements individually — that is what makes
8507/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8508/// accepted because an async generator object does not expose
8509/// `Symbol.asyncIterator` on this frontend.
8510fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8511    thread_local! {
8512        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8513    }
8514    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8515        const out = []; let i = 0;\n\
8516        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8517        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8518            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8519        if (iterable) {\n\
8520            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8521            return out;\n\
8522        }\n\
8523        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8524        while (i < len) { await step(items[i]); }\n\
8525        return out;\n\
8526    })";
8527    let f = IMPL.with(|c| c.borrow().clone());
8528    let f = match f {
8529        Some(f) => f,
8530        None => {
8531            let f = crate::eval_in_global_scope(SRC)?;
8532            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8533            f
8534        }
8535    };
8536    host::invoke(&f, args, None)
8537}
8538
8539fn array_from(args: Vec<Value>) -> Result<Value, String> {
8540    // `Array.from` accepts generators and user iterables, plus array-likes with a
8541    // numeric `.length`.
8542    let src = arg0(&args);
8543    if let Some(cb) = args.get(1).cloned() {
8544        // Stepped, not drained: the mapper runs per element as the iterator
8545        // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8546        // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8547        // all and HUNG, and a throwing mapper could not close the iterator.
8548        let this = this_arg(&args, 2);
8549        let mut out = Vec::new();
8550        let mapped = host::iter_for_each(&src, |v, i| {
8551            out.push(host::invoke(
8552                &cb,
8553                vec![v, Value::Float(i as f64)],
8554                this.clone(),
8555            )?);
8556            Ok(())
8557        });
8558        match mapped {
8559            Ok(()) => {}
8560            // An array-LIKE has no iterator; fall back to its indexed items.
8561            Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8562                out.clear();
8563                for (i, it) in array_like_items(&src).into_iter().enumerate() {
8564                    out.push(host::invoke(
8565                        &cb,
8566                        vec![it, Value::Float(i as f64)],
8567                        this.clone(),
8568                    )?);
8569                }
8570            }
8571            Err(e) => return Err(e),
8572        }
8573        return construct_array_like(host::current_static_this(), out);
8574    }
8575    let items = match host::iter_all(&src) {
8576        Ok(v) => v,
8577        Err(_) => array_like_items(&src),
8578    };
8579    // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8580    // result is an instance of it. It always allocated a plain array, which is
8581    // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8582    // never started.
8583    construct_array_like(host::current_static_this(), items)
8584}
8585
8586/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8587fn array_like_items(src: &Value) -> Vec<Value> {
8588    // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8589    // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8590    // because the infallible read does no `ToPrimitive`. A throw from it is
8591    // swallowed here for the same reason the `length` read is: this helper has
8592    // no way to report one, and every caller treats an unreadable length as 0.
8593    let len = get_property(src, "length")
8594        .ok()
8595        .and_then(|l| host::to_primitive(&l, "number").ok())
8596        .map(|l| with_host(|h| h.to_number(&l)))
8597        .unwrap_or(0.0);
8598    if !len.is_finite() || len <= 0.0 {
8599        return Vec::new();
8600    }
8601    (0..len as usize)
8602        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8603        .collect()
8604}
8605
8606// ── JSON ──────────────────────────────────────────────────────────────────────
8607
8608fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8609    // A CALLABLE second argument is the replacer function, and it is checked
8610    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8611    // callable never also reaches the key-filter path below.
8612    let replacer = args
8613        .get(1)
8614        .filter(|r| with_host(|h| host::is_callable(h, r)))
8615        .cloned();
8616    // `toJSON` and the replacer run BEFORE serialization and are user code, so
8617    // the tree is rewritten first — outside the host borrow `json_str` holds,
8618    // and before the BigInt walk, which has no cycle guard of its own.
8619    //
8620    // The top-level value is a property of a synthetic wrapper `{ "": value }`
8621    // under key `""`, which is exactly the holder the replacer receives as
8622    // `this` on its first call.
8623    let root = arg0(&args);
8624    let wrapper = with_host(|h| {
8625        let mut m: IndexMap<String, Value> = IndexMap::new();
8626        m.insert(String::new(), root.clone());
8627        h.new_object(m)
8628    });
8629    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8630    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8631    // bigint form), matching Node's exact message.
8632    if with_host(|h| json_has_bigint(h, &v)) {
8633        return Err(host::type_error("Do not know how to serialize a BigInt"));
8634    }
8635    let indent = match args.get(2) {
8636        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8637        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8638        None => String::new(),
8639    };
8640    // A replacer array (args[1]) restricts which object keys are serialized.
8641    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8642        with_host(|h| match h.get(r) {
8643            Some(JsObj::Array(items)) => {
8644                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8645            }
8646            _ => None,
8647        })
8648    });
8649    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8650    match s {
8651        Some(s) => Ok(with_host(|h| h.new_str(s))),
8652        None => Ok(Value::Undef),
8653    }
8654}
8655
8656/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8657/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8658/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8659/// object survives. Applies to user methods, class methods, and the native
8660/// `Date`/`Buffer`/`URL` accessors alike.
8661///
8662/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8663/// objects currently being walked so a cyclic structure is reported rather than
8664/// spinning forever.
8665///
8666/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8667/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8668/// because the inner method is a plain (unserializable) function property of the
8669/// returned object, not a second conversion hook.
8670fn apply_to_json(
8671    holder: &Value,
8672    key: &str,
8673    v: &Value,
8674    path: &mut Vec<Value>,
8675    rep: Option<&Value>,
8676) -> Result<Value, String> {
8677    let mut v = v.clone();
8678    if matches!(v, Value::Obj(_)) {
8679        let tag = crate::stdlib::native_tag(&v);
8680        // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8681        // supplies one through its `get` trap. `lookup_chain` walks the
8682        // property map and never asks the handler, so a proxy carrying a
8683        // `toJSON` was serialized as a plain object instead of by its own
8684        // method — and node's trap log starts with that `get`.
8685        let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8686            get_property(&v, "toJSON")?
8687        } else {
8688            with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8689        };
8690        let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8691            || tag
8692                .as_deref()
8693                .map(crate::stdlib::has_to_json)
8694                .unwrap_or(false);
8695        if has_to_json {
8696            let k = with_host(|h| h.new_str(key.to_string()));
8697            v = host::call_method(&v, "toJSON", vec![k])?;
8698        }
8699    }
8700    if let Some(rep) = rep {
8701        let k = with_host(|h| h.new_str(key.to_string()));
8702        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8703    }
8704    json_walk_children(&v, path, rep)
8705}
8706
8707/// Whether a raw property key of a host object is one `json_str` serializes. The
8708/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8709/// invisible to JSON, so the replacer must not be invoked for them either.
8710fn json_visible_key(k: &str) -> bool {
8711    !k.starts_with("@@") && !k.starts_with('#')
8712}
8713
8714/// Recurse into the elements/properties of an already-converted value, running
8715/// `apply_to_json` for each with this value as the holder.
8716fn json_walk_children(
8717    v: &Value,
8718    path: &mut Vec<Value>,
8719    rep: Option<&Value>,
8720) -> Result<Value, String> {
8721    if !matches!(v, Value::Obj(_)) {
8722        return Ok(v.clone());
8723    }
8724    // A value that contains itself has no JSON form.
8725    if with_host(|h| path.iter().any(|p| h.strict_eq(p, v))) {
8726        return Err(host::type_error("Converting circular structure to JSON"));
8727    }
8728    // A Proxy owns no property map, so it is snapshotted through its traps into
8729    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8730    // — which read every member through `[[Get]]`, exactly as the snapshot does.
8731    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8732        let snap = crate::proxy::json_snapshot(v)?;
8733        path.push(v.clone());
8734        let out = json_walk_children(&snap, path, rep);
8735        path.pop();
8736        return out;
8737    }
8738    let obj = with_host(|h| h.get(v).cloned());
8739    path.push(v.clone());
8740    let out = (|| match obj {
8741        Some(JsObj::Array(items)) => {
8742            // Read the elements through the accessor-aware funnel: an index
8743            // with a getter must be SERIALIZED as what the getter returns, and
8744            // the backing vector still holds the stale slot.
8745            let mut resolved = items;
8746            // An index with a getter must be SERIALIZED as what the getter
8747            // returns, and it also forces a rebuild below: keeping the original
8748            // array would hand the serializer back the stale backing vector.
8749            let had_accessor = resolve_index_accessors(v, &mut resolved);
8750            let items = resolved;
8751            let mut out = Vec::with_capacity(items.len());
8752            let mut changed = had_accessor;
8753            for (i, it) in items.iter().enumerate() {
8754                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8755                changed |= !with_host(|h| h.strict_eq(&nv, it));
8756                out.push(nv);
8757            }
8758            // Keep identity when nothing changed, so an enclosing object is not
8759            // needlessly rebuilt (which would drop its property attributes).
8760            if changed {
8761                Ok(with_host(|h| h.new_array(out)))
8762            } else {
8763                Ok(v.clone())
8764            }
8765        }
8766        Some(JsObj::Object(props)) => {
8767            // An enumerable own accessor must have its getter RUN and the result
8768            // serialized. That cannot happen inside `json_str` (which holds the
8769            // host borrow), so materialize here — the same reason `toJSON` is
8770            // applied in this pass.
8771            let has_accessor = with_host(|h| {
8772                h.own_accessor_keys(v)
8773                    .iter()
8774                    .any(|k| h.prop_attrs(v, k).enumerable)
8775            });
8776            if has_accessor {
8777                let mut next: IndexMap<String, Value> = IndexMap::new();
8778                for (k, val) in host::own_enum_entries_deep(v)? {
8779                    let nv = if json_visible_key(&k) {
8780                        apply_to_json(v, &k, &val, path, rep)?
8781                    } else {
8782                        val
8783                    };
8784                    next.insert(k, nv);
8785                }
8786                return Ok(with_host(|h| h.new_object(next)));
8787            }
8788            // Only rebuild when a descendant actually changed, so plain data keeps
8789            // its identity (and its prototype / native tag).
8790            let mut next: IndexMap<String, Value> = IndexMap::new();
8791            let mut changed = false;
8792            for (k, val) in &props {
8793                let nv = if json_visible_key(k) {
8794                    apply_to_json(v, k, val, path, rep)?
8795                } else {
8796                    val.clone()
8797                };
8798                changed |= !with_host(|h| h.strict_eq(&nv, val));
8799                next.insert(k.clone(), nv);
8800            }
8801            if changed {
8802                Ok(with_host(|h| {
8803                    let o = h.new_object(next);
8804                    h.copy_prop_attrs(v, &o);
8805                    o
8806                }))
8807            } else {
8808                Ok(v.clone())
8809            }
8810        }
8811        _ => Ok(v.clone()),
8812    })();
8813    path.pop();
8814    out
8815}
8816
8817/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8818/// try to serialize (a value in an array/object) — such a value throws.
8819fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8820    match h.get(v) {
8821        Some(JsObj::BigInt(_)) => true,
8822        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8823        Some(JsObj::Object(props)) => props
8824            .iter()
8825            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8826            .any(|(_, val)| json_has_bigint(h, val)),
8827        _ => false,
8828    }
8829}
8830
8831fn json_str(
8832    h: &host::JsHost,
8833    v: &Value,
8834    indent: &str,
8835    depth: usize,
8836    keys: Option<&[String]>,
8837) -> Option<String> {
8838    let sep = if indent.is_empty() { ":" } else { ": " };
8839    match v {
8840        Value::Undef => None,
8841        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8842        Value::Int(n) => Some(n.to_string()),
8843        Value::Float(f) => Some(if f.is_finite() {
8844            host::fmt_number(*f)
8845        } else {
8846            "null".into()
8847        }),
8848        Value::Str(s) => Some(json_quote(s)),
8849        Value::Obj(_) => match h.get(v) {
8850            Some(JsObj::Str(s)) => Some(json_quote(s)),
8851            Some(JsObj::Null) => Some("null".into()),
8852            // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8853            // whole point of it, and it is why a number wider than a `double`
8854            // can survive a round trip.
8855            _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8856                Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8857                _ => None,
8858            },
8859            // A Map/Set has no ENTRIES to serialize (they are internal slots),
8860            // but any own property a script attached is serialized like an
8861            // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8862            // is `{"a":1}`.
8863            Some(JsObj::Map { .. })
8864            | Some(JsObj::Set { .. })
8865            | Some(JsObj::RegExp(_))
8866            // A Promise and a generator are ORDINARY objects to the serializer:
8867            // their state is internal slots, so they contribute no entries and
8868            // render as `{}`. They were being omitted entirely instead, so a
8869            // promise in an array became `null` and one in an object vanished.
8870            | Some(JsObj::Promise { .. })
8871            | Some(JsObj::Generator { .. }) => {
8872                let parts: Vec<String> = h
8873                    .own_enum_entries(v)
8874                    .into_iter()
8875                    .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8876                    .filter_map(|(k, val)| {
8877                        json_str(h, &val, indent, depth + 1, keys)
8878                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8879                    })
8880                    .collect();
8881                Some(wrap(&parts, "{", "}", indent, depth))
8882            }
8883            // A NON-callable builtin is a namespace object, not a function, so
8884            // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8885            // are all non-enumerable), where omitting it made the whole property
8886            // disappear from its holder.
8887            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8888                let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8889                    .into_iter()
8890                    .filter_map(|k| {
8891                        let val = h.builtin_static(n, &k)?;
8892                        json_str(h, &val, indent, depth + 1, keys)
8893                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8894                    })
8895                    .collect();
8896                Some(wrap(&parts, "{", "}", indent, depth))
8897            }
8898            // Functions and symbols are omitted (undefined) as values.
8899            Some(JsObj::Func(_))
8900            | Some(JsObj::Builtin(_))
8901            | Some(JsObj::BoundMethod { .. })
8902            | Some(JsObj::BoundFunc { .. })
8903            | Some(JsObj::Class(_))
8904            | Some(JsObj::Symbol { .. }) => None,
8905            Some(JsObj::Array(items)) => {
8906                if items.is_empty() {
8907                    return Some("[]".into());
8908                }
8909                let parts: Vec<String> = items
8910                    .iter()
8911                    .map(|x| {
8912                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8913                    })
8914                    .collect();
8915                Some(wrap(&parts, "[", "]", indent, depth))
8916            }
8917            Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8918                // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8919                // the primitive it boxes, not as the object holding it —
8920                // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8921                json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8922            }
8923            Some(JsObj::Object(props)) => {
8924                // A replacer array restricts (and orders) which keys are emitted.
8925                let parts: Vec<String> = match keys {
8926                    Some(allow) => allow
8927                        .iter()
8928                        .filter_map(|k| {
8929                            props.get(k).and_then(|val| {
8930                                json_str(h, val, indent, depth + 1, keys)
8931                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8932                            })
8933                        })
8934                        .collect(),
8935                    None => h
8936                        .own_enum_entries(v)
8937                        .iter()
8938                        .filter_map(|(k, val)| {
8939                            json_str(h, val, indent, depth + 1, keys)
8940                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8941                        })
8942                        .collect(),
8943                };
8944                if parts.is_empty() {
8945                    return Some("{}".into());
8946                }
8947                Some(wrap(&parts, "{", "}", indent, depth))
8948            }
8949            _ => Some("null".into()),
8950        },
8951        _ => Some("null".into()),
8952    }
8953}
8954
8955fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
8956    if indent.is_empty() {
8957        format!("{open}{}{close}", parts.join(","))
8958    } else {
8959        let pad = indent.repeat(depth + 1);
8960        let pad_close = indent.repeat(depth);
8961        format!(
8962            "{open}\n{pad}{}\n{pad_close}{close}",
8963            parts.join(&format!(",\n{pad}"))
8964        )
8965    }
8966}
8967
8968fn json_quote(s: &str) -> String {
8969    let mut out = String::from("\"");
8970    for c in s.chars() {
8971        match c {
8972            '"' => out.push_str("\\\""),
8973            '\\' => out.push_str("\\\\"),
8974            '\n' => out.push_str("\\n"),
8975            '\t' => out.push_str("\\t"),
8976            '\r' => out.push_str("\\r"),
8977            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
8978            // Backspace and form feed were missing, so they fell through to the
8979            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
8980            // `""` where node produces `"\b"`. Both parse back to the same
8981            // string, so the difference is invisible to a round trip and shows
8982            // up only as a byte mismatch against a fixture or a checksum.
8983            '\u{8}' => out.push_str("\\b"),
8984            '\u{c}' => out.push_str("\\f"),
8985            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
8986            _ => out.push(c),
8987        }
8988    }
8989    out.push('"');
8990    out
8991}
8992
8993fn json_parse(args: Vec<Value>) -> Result<Value, String> {
8994    let s = with_host(|h| h.str_of(&arg0(&args)));
8995    let mut p = JsonParser {
8996        chars: s.chars().collect(),
8997        pos: 0,
8998        prims: Vec::new(),
8999        record: args
9000            .get(1)
9001            .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9002    };
9003    p.skip_ws();
9004    if p.peek().is_none() {
9005        return Err("SyntaxError: Unexpected end of JSON input".into());
9006    }
9007    let v = p.parse_value()?;
9008    let value_end = p.pos;
9009    p.skip_ws();
9010    // Anything after the top-level value is an error — the parser used to accept
9011    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9012    if let Some(c) = p.peek() {
9013        // V8 names the token kind only when it butts directly against the value
9014        // (`01` -> "Unexpected number at position 1"); with whitespace between
9015        // it is just a non-whitespace character (`1 2`).
9016        // Only a digit butted directly against a completed number literal —
9017        // V8's number scanner is still in number context there. `5"x"` and
9018        // `[0,1]0` exit the scanner cleanly and get the generic message.
9019        let after_number = value_end > 0
9020            && p.pos == value_end
9021            && p.chars[value_end - 1].is_ascii_digit()
9022            && c.is_ascii_digit();
9023        return Err(if after_number {
9024            p.err_at("Unexpected number", p.pos)
9025        } else {
9026            p.err_trailing(p.pos)
9027        });
9028    }
9029    // Optional reviver: walk bottom-up, transforming each (key, value).
9030    if let Some(reviver) = args
9031        .get(1)
9032        .filter(|r| with_host(|h| host::is_callable(h, r)))
9033        .cloned()
9034    {
9035        // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9036        // constructs before the walk.
9037        let root = with_host(|h| {
9038            let mut m: IndexMap<String, Value> = IndexMap::new();
9039            m.insert(String::new(), v.clone());
9040            h.new_object(m)
9041        });
9042        return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9043    }
9044    Ok(v)
9045}
9046
9047/// `JSON.parse` reviver walk: recurse into children first, then call
9048/// `reviver(key, value)`; a returned `undefined` drops the property.
9049///
9050/// The reviver runs with the HOLDER as `this` (25.5.1.1
9051/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9052/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9053/// so `this` was undefined and a reviver could not reach its siblings.
9054/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9055/// VERBATIM, so a number too large for a `double` survives a round trip
9056/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9057///
9058/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9059/// across the whole matrix, is
9060///
9061/// ```text
9062/// ""                 -> SyntaxError: Invalid value for JSON.rawJSON
9063/// leading whitespace -> the parse error for that first character
9064/// a complete literal -> ok
9065/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9066/// a broken literal   -> the parse error the scanner raised
9067/// ```
9068///
9069/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9070/// invalid-value message even though `JSON.parse` accepts the former and gives
9071/// a token error for the latter.
9072fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9073    const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9074    let s = with_host(|h| h.str_of(&arg0(&args)));
9075    if s.is_empty() {
9076        return Err(INVALID.into());
9077    }
9078    let mut p = JsonParser {
9079        chars: s.chars().collect(),
9080        pos: 0,
9081        prims: Vec::new(),
9082        record: false,
9083    };
9084    // An object or an array is rejected where it starts, as leading whitespace
9085    // is — both are "not a primitive", but node reports the token.
9086    if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9087        return Err(p.err_token(0));
9088    }
9089    p.parse_value()?;
9090    if p.pos != p.chars.len() {
9091        // A digit butted against a completed number is still in the number
9092        // scanner, so `"01"` reports the scanner's error rather than leftover
9093        // input — the same distinction `json_parse` draws for trailing text.
9094        if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9095            return Err(p.err_at("Unexpected number", p.pos));
9096        }
9097        return Err(INVALID.into());
9098    }
9099    // A null prototype and one own `rawJSON` property, frozen — the brand is a
9100    // hidden slot so `Object.keys` stays `["rawJSON"]`.
9101    Ok(with_host(|h| {
9102        let mut m: IndexMap<String, Value> = IndexMap::new();
9103        let text = h.new_str(s);
9104        m.insert("rawJSON".into(), text);
9105        let o = h.new_object(m);
9106        let null = h.null();
9107        h.set_proto(&o, null);
9108        h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9109        h.seal_object(&o, true);
9110        o
9111    }))
9112}
9113
9114/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9115/// NOT one, which is why the marker is a hidden slot rather than the property.
9116fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9117    Ok(Value::Bool(is_raw_json(&arg0(&args))))
9118}
9119
9120fn is_raw_json(v: &Value) -> bool {
9121    with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9122}
9123
9124fn json_revive(
9125    key: &str,
9126    val: Value,
9127    reviver: &Value,
9128    holder: &Value,
9129    prims: &[String],
9130    next: &mut usize,
9131) -> Result<Value, String> {
9132    // A PRIMITIVE claims the next recorded source slice before its children
9133    // would — it has none — and a container claims nothing. The walk descends in
9134    // the same order the parse produced them, so one cursor lines the two up.
9135    let is_container =
9136        with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9137    let source = if !is_container {
9138        let s = prims.get(*next).cloned();
9139        if s.is_some() {
9140            *next += 1;
9141        }
9142        s
9143    } else {
9144        None
9145    };
9146    match with_host(|h| h.get(&val).cloned()) {
9147        Some(JsObj::Array(items)) => {
9148            for i in 0..items.len() {
9149                let elem = with_host(|h| match h.get(&val) {
9150                    Some(JsObj::Array(it)) => it[i].clone(),
9151                    _ => Value::Undef,
9152                });
9153                let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9154                with_host(|h| {
9155                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9156                        it[i] = nv;
9157                    }
9158                });
9159            }
9160        }
9161        Some(JsObj::Object(props)) => {
9162            let keys: Vec<String> = props
9163                .keys()
9164                .filter(|k| !k.starts_with("@@"))
9165                .cloned()
9166                .collect();
9167            for k in keys {
9168                let elem = with_host(|h| match h.get(&val) {
9169                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9170                    _ => Value::Undef,
9171                });
9172                let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9173                with_host(|h| {
9174                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9175                        if matches!(nv, Value::Undef) {
9176                            p.shift_remove(&k);
9177                        } else {
9178                            p.insert(k.clone(), nv);
9179                        }
9180                    }
9181                });
9182            }
9183        }
9184        _ => {}
9185    }
9186    let kv = with_host(|h| h.new_str(key.to_string()));
9187    // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9188    // primitive, an empty object for an array or an object — node passes it
9189    // either way, and code reading `ctx.source` used to die on `undefined`
9190    // because only two arguments were passed.
9191    let ctx = with_host(|h| {
9192        let mut m: IndexMap<String, Value> = IndexMap::new();
9193        if let Some(s) = source {
9194            let sv = h.new_str(s);
9195            m.insert("source".into(), sv);
9196        }
9197        h.new_object(m)
9198    });
9199    host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9200}
9201
9202struct JsonParser {
9203    chars: Vec<char>,
9204    pos: usize,
9205    /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9206    /// third argument reports as `context.source` (25.5.1.1). Only collected
9207    /// when a reviver was supplied.
9208    ///
9209    /// A flat list rather than a parallel tree because the reviver walk visits
9210    /// primitives in the same depth-first order the parse produced them, so an
9211    /// index into this is enough to line them up.
9212    prims: Vec<String>,
9213    record: bool,
9214}
9215impl JsonParser {
9216    fn peek(&self) -> Option<char> {
9217        self.chars.get(self.pos).copied()
9218    }
9219
9220    /// `at position N (line L column C)` — the location suffix V8 appends to the
9221    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9222    /// node-js counts `char`s, which agree for the BMP.
9223    fn at(&self, pos: usize) -> String {
9224        let mut line = 1usize;
9225        let mut col = 1usize;
9226        for c in &self.chars[..pos.min(self.chars.len())] {
9227            if *c == '\n' {
9228                line += 1;
9229                col = 1;
9230            } else {
9231                col += 1;
9232            }
9233        }
9234        format!(" at position {pos} (line {line} column {col})")
9235    }
9236
9237    /// A positional error (`Expected ':' after property name in JSON at …`).
9238    fn err_at(&self, what: &str, pos: usize) -> String {
9239        format!("SyntaxError: {what} in JSON{}", self.at(pos))
9240    }
9241
9242    /// The one positional message V8 does NOT suffix with `in JSON`.
9243    fn err_trailing(&self, pos: usize) -> String {
9244        format!(
9245            "SyntaxError: Unexpected non-whitespace character after JSON{}",
9246            self.at(pos)
9247        )
9248    }
9249
9250    /// V8's default parse error: the offending character plus a window of the
9251    /// source. The whole input is quoted when it is short (<= 20 chars);
9252    /// otherwise a 10-character context window either side of `pos` is shown,
9253    /// elided with `...` on whichever side was cut.
9254    fn err_token(&self, pos: usize) -> String {
9255        const MAX_WHOLE: usize = 20;
9256        const CONTEXT: usize = 10;
9257        let len = self.chars.len();
9258        let Some(c) = self.chars.get(pos) else {
9259            return "SyntaxError: Unexpected end of JSON input".into();
9260        };
9261        // V8 reports the whole input for the JS literals that are famously not
9262        // JSON, without naming an offending character.
9263        let whole: String = self.chars.iter().collect();
9264        if matches!(
9265            whole.as_str(),
9266            "undefined" | "NaN" | "Infinity" | "-Infinity"
9267        ) {
9268            return format!("SyntaxError: \"{whole}\" is not valid JSON");
9269        }
9270        let snippet = if len <= MAX_WHOLE {
9271            format!("\"{whole}\"")
9272        } else {
9273            let start = pos.saturating_sub(CONTEXT);
9274            let end = (pos + CONTEXT).min(len);
9275            let body: String = self.chars[start..end].iter().collect();
9276            let head = if start > 0 { "..." } else { "" };
9277            let tail = if end < len { "..." } else { "" };
9278            format!("{head}\"{body}\"{tail}")
9279        };
9280        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9281    }
9282
9283    fn skip_ws(&mut self) {
9284        while matches!(
9285            self.peek(),
9286            Some(' ') | Some('\n') | Some('\t') | Some('\r')
9287        ) {
9288            self.pos += 1;
9289        }
9290    }
9291    fn parse_value(&mut self) -> Result<Value, String> {
9292        self.skip_ws();
9293        let start = self.pos;
9294        let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9295        let v = match self.peek() {
9296            Some('{') => self.parse_object(),
9297            Some('[') => self.parse_array(),
9298            Some('"') => {
9299                let s = self.parse_string()?;
9300                Ok(with_host(|h| h.new_str(s)))
9301            }
9302            Some('t') | Some('f') => self.parse_bool(),
9303            Some('n') => {
9304                self.expect_lit("null")?;
9305                Ok(with_host(|h| h.null()))
9306            }
9307            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9308            None => Err("SyntaxError: Unexpected end of JSON input".into()),
9309            _ => Err(self.err_token(self.pos)),
9310        }?;
9311        if prim && self.record {
9312            self.prims
9313                .push(self.chars[start..self.pos].iter().collect());
9314        }
9315        Ok(v)
9316    }
9317    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9318        for ch in lit.chars() {
9319            match self.peek() {
9320                Some(c) if c == ch => self.pos += 1,
9321                // V8 reports the first character that broke the literal, which is
9322                // why `foo` complains about `'o'` (index 2) and not `'f'`.
9323                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9324                _ => return Err(self.err_token(self.pos)),
9325            }
9326        }
9327        Ok(())
9328    }
9329    fn parse_bool(&mut self) -> Result<Value, String> {
9330        if self.peek() == Some('t') {
9331            self.expect_lit("true")?;
9332            Ok(Value::Bool(true))
9333        } else {
9334            self.expect_lit("false")?;
9335            Ok(Value::Bool(false))
9336        }
9337    }
9338    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9339    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9340    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9341    /// it. Each way the grammar can run out has its own message.
9342    fn parse_number(&mut self) -> Result<Value, String> {
9343        let start = self.pos;
9344        if self.peek() == Some('-') {
9345            self.pos += 1;
9346            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9347                return Err(self.err_at("No number after minus sign", self.pos));
9348            }
9349        }
9350        if self.peek() == Some('0') {
9351            self.pos += 1;
9352        } else {
9353            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9354                self.pos += 1;
9355            }
9356        }
9357        if self.peek() == Some('.') {
9358            self.pos += 1;
9359            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9360                return Err(self.err_at("Unterminated fractional number", self.pos));
9361            }
9362            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9363                self.pos += 1;
9364            }
9365        }
9366        if matches!(self.peek(), Some('e') | Some('E')) {
9367            self.pos += 1;
9368            if matches!(self.peek(), Some('+') | Some('-')) {
9369                self.pos += 1;
9370            }
9371            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9372                return Err(self.err_at("Exponent part is missing a number", self.pos));
9373            }
9374            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9375                self.pos += 1;
9376            }
9377        }
9378        let s: String = self.chars[start..self.pos].iter().collect();
9379        s.parse::<f64>()
9380            .map(Value::Float)
9381            .map_err(|_| self.err_at("Unexpected number", start))
9382    }
9383    fn parse_string(&mut self) -> Result<String, String> {
9384        self.pos += 1; // opening quote
9385        let mut out = String::new();
9386        loop {
9387            match self.peek() {
9388                None => return Err(self.err_at("Unterminated string", self.pos)),
9389                Some('"') => {
9390                    self.pos += 1;
9391                    break;
9392                }
9393                Some('\\') => {
9394                    self.pos += 1;
9395                    match self.peek() {
9396                        Some('n') => out.push('\n'),
9397                        Some('t') => out.push('\t'),
9398                        Some('r') => out.push('\r'),
9399                        Some('"') => out.push('"'),
9400                        Some('\\') => out.push('\\'),
9401                        Some('/') => out.push('/'),
9402                        Some('b') => out.push('\u{08}'),
9403                        Some('f') => out.push('\u{0C}'),
9404                        Some('u') => {
9405                            let h: String = self.chars
9406                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9407                                .iter()
9408                                .collect();
9409                            if let Ok(n) = u32::from_str_radix(&h, 16) {
9410                                if let Some(ch) = char::from_u32(n) {
9411                                    out.push(ch);
9412                                }
9413                            }
9414                            self.pos += 4;
9415                        }
9416                        _ => {}
9417                    }
9418                    self.pos += 1;
9419                }
9420                // A raw control character is not legal inside a JSON string; it
9421                // has to be escaped. V8 rejects it rather than passing it through.
9422                Some(c) if (c as u32) < 0x20 => {
9423                    return Err(self.err_at("Bad control character in string literal", self.pos))
9424                }
9425                Some(c) => {
9426                    out.push(c);
9427                    self.pos += 1;
9428                }
9429            }
9430        }
9431        Ok(out)
9432    }
9433    fn parse_array(&mut self) -> Result<Value, String> {
9434        self.pos += 1; // [
9435        let mut items = Vec::new();
9436        self.skip_ws();
9437        if self.peek() == Some(']') {
9438            self.pos += 1;
9439            return Ok(with_host(|h| h.new_array(items)));
9440        }
9441        loop {
9442            items.push(self.parse_value()?);
9443            self.skip_ws();
9444            match self.peek() {
9445                Some(',') => {
9446                    self.pos += 1;
9447                }
9448                Some(']') => {
9449                    self.pos += 1;
9450                    break;
9451                }
9452                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9453            }
9454        }
9455        Ok(with_host(|h| h.new_array(items)))
9456    }
9457    fn parse_object(&mut self) -> Result<Value, String> {
9458        self.pos += 1; // {
9459        let mut props: IndexMap<String, Value> = IndexMap::new();
9460        self.skip_ws();
9461        if self.peek() == Some('}') {
9462            self.pos += 1;
9463            return Ok(with_host(|h| h.new_object(props)));
9464        }
9465        loop {
9466            self.skip_ws();
9467            if self.peek() != Some('"') {
9468                // The first key uses the "or '}'" wording (an empty object is
9469                // still legal there); a key after a comma does not. End of input
9470                // reports the same expectation, at the end position.
9471                return Err(if props.is_empty() {
9472                    self.err_at("Expected property name or '}'", self.pos)
9473                } else {
9474                    self.err_at("Expected double-quoted property name", self.pos)
9475                });
9476            }
9477            let key = self.parse_string()?;
9478            self.skip_ws();
9479            if self.peek() != Some(':') {
9480                return Err(match self.peek() {
9481                    None => "SyntaxError: Unexpected end of JSON input".into(),
9482                    _ => self.err_at("Expected ':' after property name", self.pos),
9483                });
9484            }
9485            self.pos += 1;
9486            let val = self.parse_value()?;
9487            props.insert(key, val);
9488            self.skip_ws();
9489            match self.peek() {
9490                Some(',') => {
9491                    self.pos += 1;
9492                }
9493                Some('}') => {
9494                    self.pos += 1;
9495                    break;
9496                }
9497                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9498            }
9499        }
9500        Ok(with_host(|h| h.new_object(props)))
9501    }
9502}
9503
9504// ══ type methods (array / string / number) ═══════════════════════════════════
9505
9506fn is_array_method(name: &str) -> bool {
9507    matches!(
9508        name,
9509        "push"
9510            | "pop"
9511            | "shift"
9512            | "unshift"
9513            | "map"
9514            | "filter"
9515            | "forEach"
9516            | "join"
9517            | "slice"
9518            | "indexOf"
9519            | "lastIndexOf"
9520            | "includes"
9521            | "reduce"
9522            | "concat"
9523            | "reverse"
9524            | "sort"
9525            | "find"
9526            | "findIndex"
9527            | "some"
9528            | "every"
9529            | "flat"
9530            | "fill"
9531            | "splice"
9532            | "keys"
9533            | "values"
9534            | "entries"
9535            | "flatMap"
9536            | "at"
9537            | "toString"
9538            | "reduceRight"
9539            | "findLast"
9540            | "findLastIndex"
9541            | "copyWithin"
9542    )
9543}
9544/// Every `String.prototype` method node-js implements.
9545///
9546/// A LIST rather than a `matches!` arm because the same set has to be installed
9547/// on the real `String.prototype` object: a method read off the prototype
9548/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9549/// found nothing there, so the two views of "which methods exist" would drift
9550/// if they were written twice.
9551pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9552    "toUpperCase",
9553    "toLowerCase",
9554    "charAt",
9555    "charCodeAt",
9556    "codePointAt",
9557    "indexOf",
9558    "lastIndexOf",
9559    "includes",
9560    "slice",
9561    "substring",
9562    "substr",
9563    "split",
9564    "trim",
9565    "trimStart",
9566    "trimEnd",
9567    "replace",
9568    "replaceAll",
9569    "repeat",
9570    "startsWith",
9571    "endsWith",
9572    "padStart",
9573    "padEnd",
9574    "concat",
9575    "at",
9576    "toString",
9577    "toLocaleString",
9578    "valueOf",
9579    "match",
9580    "matchAll",
9581    "search",
9582    "normalize",
9583    "localeCompare",
9584    "toLocaleUpperCase",
9585    "toLocaleLowerCase",
9586    "isWellFormed",
9587    "toWellFormed",
9588];
9589
9590fn is_string_method(name: &str) -> bool {
9591    STRING_PROTO_METHODS.contains(&name)
9592}
9593
9594/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9595/// spelled the way this frontend spells the key (`@@iterator`).
9596///
9597/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9598/// .prototype`) installs its methods from a list, and only the string-keyed
9599/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9600/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9601/// `Builtin` namespace and its table gate, answered a function. Derived from
9602/// the table rather than written out, so it cannot name a method node does not
9603/// define nor miss one it does.
9604pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9605    let prefix = format!("@proto:{ctor}:");
9606    crate::arity::BUILTIN_ARITY
9607        .iter()
9608        .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9609        .filter(|m| m.starts_with("@@"))
9610        .collect()
9611}
9612
9613/// The builtin constructors whose `.prototype` object is BRANDED — every other
9614/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9615///
9616/// Measured on node v26.8.1 over every constructor this frontend knows:
9617///
9618/// ```text
9619/// Array/Object/Number/String/Boolean/Function   the ES5 legacy slot prototypes
9620/// Symbol/BigInt/Map/Set/WeakMap/WeakSet         carry an own @@toStringTag
9621/// Promise/Iterator/ArrayBuffer/DataView         "
9622/// WeakRef/FinalizationRegistry/URL              "
9623/// URLSearchParams/TextEncoder/TextDecoder       "
9624/// Date/RegExp/Error/TypeError/Uint8Array/…      [object Object]
9625/// ```
9626///
9627/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9628/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9629/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9630/// its own failure message where node names `[object Object]`.
9631pub(crate) const BRANDED_PROTOS: &[&str] = &[
9632    "Array",
9633    "ArrayBuffer",
9634    "BigInt",
9635    "Boolean",
9636    "DataView",
9637    "FinalizationRegistry",
9638    "Function",
9639    "Iterator",
9640    "Map",
9641    "Number",
9642    "Object",
9643    "Promise",
9644    "Set",
9645    "SharedArrayBuffer",
9646    "String",
9647    "Symbol",
9648    "TextDecoder",
9649    "TextEncoder",
9650    "URL",
9651    "URLSearchParams",
9652    "WeakMap",
9653    "WeakRef",
9654    "WeakSet",
9655];
9656
9657/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9658/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9659/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9660///
9661/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9662/// than coercing `o` to a pattern — the protocol every regexp-like library
9663/// implements. None of the five were consulted, so a custom matcher was
9664/// silently stringified instead.
9665fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9666    if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9667        return None;
9668    }
9669    let f = get_property(arg, sym).ok()?;
9670    with_host(|h| host::is_callable(h, &f)).then_some(f)
9671}
9672
9673fn is_regexp_arg(v: &Value) -> bool {
9674    // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9675    // itself a regexp — or a real one can disown the label. Only the heap kind
9676    // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9677    // the TypeError the spec requires.
9678    if let Ok(m) = get_property(v, "@@match") {
9679        if !matches!(m, Value::Undef) {
9680            return with_host(|h| h.truthy(&m));
9681        }
9682    }
9683    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9684}
9685
9686/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9687/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9688fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9689    if pat.is_empty() {
9690        return Ok(s.to_string());
9691    }
9692    let mut out = String::new();
9693    let mut rest = s;
9694    let mut base = 0usize;
9695    while let Some(pos) = rest.find(pat) {
9696        out.push_str(&rest[..pos]);
9697        let offset = base + pos;
9698        let m = with_host(|h| h.new_str(pat.to_string()));
9699        let str_arg = with_host(|h| h.new_str(s.to_string()));
9700        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9701        out.push_str(&with_host(|h| h.str_of(&r)));
9702        let consumed = pos + pat.len();
9703        base += consumed;
9704        rest = &rest[consumed..];
9705        if !all {
9706            break;
9707        }
9708    }
9709    out.push_str(rest);
9710    Ok(out)
9711}
9712/// Every `Number.prototype` method node-js implements — a list for the same
9713/// reason [`STRING_PROTO_METHODS`] is one.
9714pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9715    "toFixed",
9716    "toExponential",
9717    "toString",
9718    "toPrecision",
9719    "toLocaleString",
9720    "valueOf",
9721];
9722
9723fn is_number_method(name: &str) -> bool {
9724    NUMBER_PROTO_METHODS.contains(&name)
9725}
9726
9727/// The exotic kinds whose own dispatch table does NOT already reach the
9728/// `Object.prototype` methods, so the inherited ones have to be routed to.
9729///
9730/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9731/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9732/// it to the object form made `(255).toString(16)` report `[object Number]`.
9733fn inherits_object_methods(recv: &Value) -> bool {
9734    matches!(
9735        with_host(|h| h.kind_of(recv)),
9736        Some(
9737            ObjKind::Map
9738                | ObjKind::Set
9739                | ObjKind::Promise
9740                | ObjKind::RegExp
9741                | ObjKind::Generator
9742                | ObjKind::Symbol
9743                | ObjKind::BigInt
9744                | ObjKind::Iter
9745        )
9746    )
9747}
9748
9749/// Whether `recv`'s own prototype defines `name`, shadowing the
9750/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9751/// `Map.prototype` does not.
9752fn overrides_object_method(recv: &Value, name: &str) -> bool {
9753    match with_host(|h| h.kind_of(recv)) {
9754        Some(ObjKind::Map) => is_map_method(name),
9755        Some(ObjKind::Set) => is_set_method(name),
9756        Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9757        // A Symbol has its own `toString`; `valueOf` is the inherited one,
9758        // which returns the receiver — exactly what a symbol needs.
9759        Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9760        Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9761        _ => false,
9762    }
9763}
9764
9765/// Dispatch `recv.name(args)` for the built-in prototype methods.
9766pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9767    // A USER method on the receiver's prototype chain wins over the builtin of
9768    // the same name — that is how a `class X extends Array` method is reached,
9769    // since the dispatch below goes straight to the builtin table and has no
9770    // entry for it.
9771    //
9772    // Deliberately restricted to a user function: the shared `Object.prototype`
9773    // carries real `@proto:Object:*` thunks, so accepting any callable made a
9774    // bare `map.toString()` resolve to the object form instead of the builtin
9775    // one the exotic is supposed to use.
9776    if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9777        if matches!(
9778            with_host(|h| h.kind_of(&f)),
9779            Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9780        ) {
9781            return host::invoke(&f, args, Some(recv.clone()));
9782        }
9783    }
9784    // A method synthesized from the receiver's KIND is unreachable once its
9785    // intrinsic prototype is off the chain. The read already answers
9786    // `undefined` for one; dispatch has its own table and would still have
9787    // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9788    // while `a.join` was `undefined` — the read and the call disagreeing again,
9789    // in the opposite direction from the monkey-patch case below.
9790    if !own_intrinsic_reachable(recv)
9791        && inherited_method_owner(recv, name).is_none()
9792        && !has_own_for_shadow(recv, name)
9793        && inherited_builtin_static(recv, name).is_none()
9794        && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9795    {
9796        return Err(host::type_error(&format!("{name} is not a function")));
9797    }
9798    // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9799    // path resolves these, but dispatch goes straight to the builtin table and
9800    // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9801    // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9802    // about the same name, on the one path a polyfill actually uses.
9803    if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9804        if let Some(f) = inherited_builtin_static(recv, name) {
9805            if with_host(|h| host::is_callable(h, &f)) {
9806                return host::invoke(&f, args, Some(recv.clone()));
9807            }
9808        }
9809    }
9810    // Every object INHERITS the `Object.prototype` methods, and an exotic that
9811    // does not define its own reaches them the same way. Each kind's dispatch
9812    // table below only knows its own methods, so `new Map().toString()`,
9813    // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9814    // not a function" — `Object.prototype.toString.call(m)` worked while
9815    // `m.toString()` did not.
9816    // The allowlist is the kinds whose own dispatch table below would otherwise
9817    // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9818    // the same way — a function, a class and a bound function included, where
9819    // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9820    // resolved it. `inherited_method_owner` decides which prototype owns the
9821    // name, so a kind that defines its own still gets its own.
9822    if is_object_builtin_method(name)
9823        && (inherited_method_owner(recv, name) == Some("Object")
9824            || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9825    {
9826        // `toString` goes through the branded form (20.1.3.6), which reads
9827        // `Symbol.toStringTag` and falls back to the receiver's own brand —
9828        // `[object Map]`, not the generic stringification.
9829        if name == "toString" {
9830            return proto_method(recv, "Object:toString", args);
9831        }
9832        return object_builtin_method(recv, name, args);
9833    }
9834    // `Object.prototype.valueOf` is inherited by every exotic that does not
9835    // override it (an Array does not), and returns the receiver. Without this
9836    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9837    // and threw `valueOf is not a function`.
9838    if name == "valueOf"
9839        && matches!(
9840            with_host(|h| h.kind_of(recv)),
9841            Some(
9842                ObjKind::Array
9843                    | ObjKind::Map
9844                    | ObjKind::Set
9845                    | ObjKind::Generator
9846                    | ObjKind::Promise
9847                    | ObjKind::Iter
9848                    | ObjKind::RegExp
9849            )
9850        )
9851    {
9852        return Ok(recv.clone());
9853    }
9854    // Only the tag is needed to pick the branch — cloning the receiver here made
9855    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9856    match with_host(|h| h.kind_of(recv)) {
9857        Some(ObjKind::Array) => array_method(recv, name, args),
9858        Some(ObjKind::Str) => {
9859            // `string_method` consumes the text itself, so this clone is the
9860            // payload, not a tag probe.
9861            let s = peek(recv, |o| match o {
9862                JsObj::Str(s) => Some(s.clone()),
9863                _ => None,
9864            })
9865            .unwrap_or_default();
9866            string_method(&s, name, args)
9867        }
9868        Some(ObjKind::Map) => map_method(recv, name, args),
9869        Some(ObjKind::Set) => set_method(recv, name, args),
9870        Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9871            crate::stdlib::iterator::call(recv, name, &args)
9872        }
9873        Some(ObjKind::Generator) => generator_method(recv, name, args),
9874        Some(ObjKind::Promise) => promise_method(recv, name, args),
9875        Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9876            crate::stdlib::iterator::call(recv, name, &args)
9877        }
9878        Some(ObjKind::Iter) => iter_method(recv, name, args),
9879        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9880        Some(ObjKind::BigInt) => {
9881            let b = peek(recv, |o| match o {
9882                JsObj::BigInt(b) => Some(b.clone()),
9883                _ => None,
9884            })
9885            .unwrap_or_default();
9886            bigint_method(&b, name, args)
9887        }
9888        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9889        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9890            match function_builtin_method(recv, name, &args)? {
9891                Some(v) => Ok(v),
9892                None => Err(host::type_error(&format!("{name} is not a function"))),
9893            }
9894        }
9895        Some(ObjKind::Object) => {
9896            if let Some(f) = peek(recv, |o| match o {
9897                JsObj::Object(p) => p.get(name).cloned(),
9898                _ => None,
9899            }) {
9900                host::invoke(&f, args, Some(recv.clone()))
9901            } else if name == "hasOwnProperty" {
9902                let k = with_host(|h| h.str_of(&arg0(&args)));
9903                let has = peek(recv, |o| match o {
9904                    JsObj::Object(p) => Some(p.contains_key(&k)),
9905                    _ => None,
9906                })
9907                .unwrap_or(false);
9908                Ok(Value::Bool(has))
9909            } else if name == "toString" {
9910                Ok(with_host(|h| h.new_str("[object Object]")))
9911            } else {
9912                Err(host::type_error(&format!("{} is not a function", name)))
9913            }
9914        }
9915        _ => {
9916            // Primitive number/bool/string coercions.
9917            if let Value::Float(_) | Value::Int(_) = recv {
9918                return number_method(with_host(|h| h.to_number(recv)), name, args);
9919            }
9920            if let Some(s) = with_host(|h| h.as_str(recv)) {
9921                return string_method(&s, name, args);
9922            }
9923            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9924            // so it reached no branch at all and `true.toString()` threw `is not
9925            // a function`. Its three methods are `toString`, `valueOf`, and the
9926            // inherited `Object.prototype.toLocaleString` — which
9927            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9928            // was reachable from the array form too.
9929            if let Value::Bool(b) = recv {
9930                return match name {
9931                    "toString" | "toLocaleString" => {
9932                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
9933                    }
9934                    "valueOf" => Ok(Value::Bool(*b)),
9935                    _ => Err(host::type_error(&format!("{name} is not a function"))),
9936                };
9937            }
9938            Err(host::type_error(&format!("{} is not a function", name)))
9939        }
9940    }
9941}
9942
9943/// A copy of the whole backing store, for the methods that genuinely consume
9944/// every element (`map`, `filter`, `join`, …). Never call it just to read
9945/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
9946/// A LIVE iterator over a `Map` or `Set`.
9947///
9948/// Node's collection iterators see the collection as it is at each step: an
9949/// entry added during iteration IS visited, and one deleted before it is
9950/// reached is NOT. Ours materialized every entry up front, so both were wrong —
9951/// a loop that deletes as it goes still processed the entries it had removed.
9952///
9953/// The cursor is the last key yielded plus the index it was at. On each step
9954/// the key is located again in the CURRENT order: if it is still there the next
9955/// entry follows it, and if it was itself deleted the stored index now names
9956/// the entry that shifted into its place. That reproduces node for the cases
9957/// its own tests turn on — add-during, delete-ahead, delete-self,
9958/// delete-behind, delete-the-rest and clear — without giving `Map` the
9959/// tombstoned entry list node uses internally.
9960fn collection_iterator(coll: &Value, kind: &str) -> Value {
9961    with_host(|h| {
9962        let mut m = IndexMap::new();
9963        m.insert(
9964            "@@native".into(),
9965            h.new_str("CollectionIterator".to_string()),
9966        );
9967        m.insert("@@coll".into(), coll.clone());
9968        m.insert("@@kind".into(), h.new_str(kind.to_string()));
9969        m.insert("@@started".into(), Value::Bool(false));
9970        m.insert("@@lastIdx".into(), Value::Float(0.0));
9971        h.new_object(m)
9972    })
9973}
9974
9975/// One step of a live collection iterator.
9976pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
9977    let slot = |k: &str| {
9978        with_host(|h| match h.get(recv) {
9979            Some(JsObj::Object(p)) => p.get(k).cloned(),
9980            _ => None,
9981        })
9982    };
9983    let coll = slot("@@coll").unwrap_or(Value::Undef);
9984    let kind = slot("@@kind")
9985        .map(|v| with_host(|h| h.str_of(&v)))
9986        .unwrap_or_default();
9987    let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
9988    let last_idx = slot("@@lastIdx")
9989        .map(|v| with_host(|h| h.to_number(&v)) as usize)
9990        .unwrap_or(0);
9991    let last_key = slot("@@lastKey");
9992
9993    let next_idx = if !started {
9994        0
9995    } else {
9996        match last_key
9997            .as_ref()
9998            .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
9999        {
10000            // Still present: continue after it.
10001            Some(i) => i + 1,
10002            // Deleted since: whatever shifted into its slot is next.
10003            None => last_idx,
10004        }
10005    };
10006    let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10007    let Some((k, v)) = entry else {
10008        return Ok(iter_result(Value::Undef, true));
10009    };
10010    with_host(|h| {
10011        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10012            p.insert("@@started".into(), Value::Bool(true));
10013            p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10014            p.insert("@@lastKey".into(), k.clone());
10015        }
10016    });
10017    let out = match kind.as_str() {
10018        "keys" => k,
10019        "values" => v,
10020        _ => with_host(|h| h.new_array(vec![k, v])),
10021    };
10022    Ok(iter_result(out, false))
10023}
10024
10025/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10026fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10027    match h.get(coll) {
10028        Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10029        Some(JsObj::Set { entries, .. }) => {
10030            entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10031        }
10032        _ => None,
10033    }
10034}
10035
10036/// Where `key` currently sits in the collection's order.
10037fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10038    let mk = host::map_key(h, key);
10039    match h.get(coll) {
10040        Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10041        Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10042        _ => None,
10043    }
10044}
10045
10046/// The `thisArg` an iteration method was given, if any.
10047///
10048/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10049/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10050/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10051/// `Array.from`'s map function. Every one of them was invoking the callback
10052/// with no receiver, so `this` inside it was undefined and the argument did
10053/// nothing.
10054fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10055    args.get(idx)
10056        .filter(|v| !matches!(v, Value::Undef))
10057        .cloned()
10058}
10059
10060/// The elements of an array, with any INDEX ACCESSOR resolved.
10061///
10062/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10063/// table, and an array's elements live in a backing vector — so every method
10064/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10065/// slot and never called the getter, while a plain `arr[1]` read did.
10066///
10067/// An array with no accessors pays one lookup returning an empty list, so the
10068/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10069/// since calling one re-enters.
10070/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10071/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10072/// read LIVE at its index, and an index that no longer exists is skipped.
10073///
10074/// Snapshotting the whole array instead meant a callback that mutated it was
10075/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10076/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10077///
10078/// `f` returns `Some(x)` to stop early with `x`.
10079fn array_walk<T>(
10080    recv: &Value,
10081    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10082) -> Result<Option<T>, String> {
10083    let len = array_len(recv);
10084    for i in 0..len {
10085        // A HOLE — and an index a shrinking mutation has dropped — is skipped
10086        // without calling the callback.
10087        if index_absent(recv, i) || i >= array_len(recv) {
10088            continue;
10089        }
10090        let v = get_property(recv, &i.to_string())?;
10091        if let Some(out) = f(i, v)? {
10092            return Ok(Some(out));
10093        }
10094    }
10095    Ok(None)
10096}
10097
10098/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10099/// capture-length-once, read-each-element-live rule walked from the end. A
10100/// callback that SHRINKS the array is observed by every later step, so the
10101/// indices it drops are skipped rather than served from a stale copy.
10102fn array_walk_rev<T>(
10103    recv: &Value,
10104    from: usize,
10105    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10106) -> Result<Option<T>, String> {
10107    for i in (0..from).rev() {
10108        if index_absent(recv, i) || i >= array_len(recv) {
10109            continue;
10110        }
10111        let v = get_property(recv, &i.to_string())?;
10112        if let Some(out) = f(i, v)? {
10113            return Ok(Some(out));
10114        }
10115    }
10116    Ok(None)
10117}
10118
10119/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10120/// `undefined` once a mutation has shrunk the array past it.
10121fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10122    if i >= array_len(recv) {
10123        return Ok(Value::Undef);
10124    }
10125    get_property(recv, &i.to_string())
10126}
10127
10128fn array_items(recv: &Value) -> Vec<Value> {
10129    let mut items = with_host(|h| match h.get(recv) {
10130        Some(JsObj::Array(items)) => items.clone(),
10131        _ => Vec::new(),
10132    });
10133    resolve_index_accessors(recv, &mut items);
10134    items
10135}
10136
10137/// Replace each slot that has an own accessor with what its getter returns.
10138pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10139    resolve_index_accessors(recv, items);
10140}
10141
10142/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10143/// the ORIGINAL array when nothing changed, and the original still holds the
10144/// stale slots.
10145fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10146    let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10147        .into_iter()
10148        .filter_map(|k| k.parse::<usize>().ok())
10149        .filter(|i| *i < items.len())
10150        .collect();
10151    // An ELIDED index the prototype chain supplies is stale in the backing
10152    // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10153    // value. Spread and `JSON.stringify` both read through here, and both
10154    // rendered the hole rather than what `a[i]` reads.
10155    let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10156        .into_iter()
10157        .filter(|i| *i < items.len() && !indices.contains(i))
10158        .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10159        .collect();
10160    indices.extend(inherited);
10161    let mut replaced = false;
10162    for i in indices {
10163        if let Ok(v) = get_property(recv, &i.to_string()) {
10164            items[i] = v;
10165            replaced = true;
10166        }
10167    }
10168    replaced
10169}
10170
10171/// The ELIDED positions of array `recv` as a membership set. A dense array —
10172/// which is nearly every array — answers with an empty set after a single
10173/// negative hash probe and allocates nothing.
10174///
10175/// The iteration methods split into two groups, and the split is not a matter of
10176/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10177/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10178/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10179/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10180fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10181    with_host(|h| h.hole_indices(recv)).into_iter().collect()
10182}
10183
10184/// The indices `recv` genuinely has NO property at — the elided ones the
10185/// prototype chain does not supply either.
10186///
10187/// Every array method tests `HasProperty` before deciding to skip a position
10188/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10189/// alone made an inherited element invisible to all of them: with
10190/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10191/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10192/// the same position.
10193///
10194/// `hole_set` remains the elision record itself, which is what `splice` moves
10195/// around — that bookkeeping is about the array's OWN storage and must not
10196/// consult the chain.
10197fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10198    hole_set(recv)
10199        .into_iter()
10200        .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10201        .collect()
10202}
10203
10204/// The single-index form of [`absent_set`], for the walkers that test one
10205/// position at a time.
10206fn index_absent(recv: &Value, i: usize) -> bool {
10207    with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10208}
10209
10210/// The element count, without copying the elements.
10211fn array_len(recv: &Value) -> usize {
10212    peek(recv, |o| match o {
10213        JsObj::Array(items) => Some(items.len()),
10214        _ => None,
10215    })
10216    .unwrap_or(0)
10217}
10218
10219/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10220/// array method builds its RESULT with.
10221///
10222/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10223/// produce an array of the receiver's own species, so on a `class A extends
10224/// Array` the result is an `A`. Every one of them allocated a plain array
10225/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10226///
10227/// The default `get [Symbol.species]() { return this }` is what makes the
10228/// subclass the species; a class overriding it with `Array` gets a plain array
10229/// back, which is the documented way to opt out.
10230/// Build an array-shaped result through `ctor`, or a plain array when there is
10231/// none to build through.
10232///
10233/// The constructor is called with the LENGTH and the elements written after, as
10234/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10235/// constructor observe the allocation.
10236fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10237    let Some(ctor) = ctor.filter(|c| {
10238        matches!(
10239            with_host(|h| h.kind_of(c)),
10240            Some(ObjKind::Class) | Some(ObjKind::Func)
10241        )
10242    }) else {
10243        return Ok(with_host(|h| h.new_array(items)));
10244    };
10245    let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10246    write_elements(&out, items);
10247    Ok(out)
10248}
10249
10250/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10251/// hole marks the length-only construction left behind.
10252///
10253/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10254/// the elements written over them stayed marked — so every subclass result of
10255/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10256/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10257fn write_elements(out: &Value, items: Vec<Value>) {
10258    with_host(|h| {
10259        h.clear_holes(out);
10260        if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10261            *dst = items;
10262        }
10263    });
10264}
10265
10266fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10267    let plain = || with_host(|h| h.new_array(items.clone()));
10268    // Only a subclass instance can have a species of its own: a plain array's
10269    // `constructor` is the `Array` builtin, whose species is `Array`.
10270    // A chain lookup, not `get_property`: an Array receiver resolves its
10271    // properties through the stdlib funnel, which has no `constructor` entry,
10272    // so the read alone reports `undefined` for every subclass instance. A
10273    // Proxy is the exception — it has no property map to walk, and its
10274    // `constructor` comes from the `get` trap, so a proxied subclass array
10275    // produced plain arrays.
10276    let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10277        get_property(recv, "constructor").unwrap_or(Value::Undef)
10278    } else {
10279        with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10280    };
10281    if !matches!(
10282        with_host(|h| h.kind_of(&ctor)),
10283        Some(ObjKind::Class) | Some(ObjKind::Func)
10284    ) {
10285        return Ok(plain());
10286    }
10287    // An explicit `@@species` wins; absent one, the constructor itself is the
10288    // species, as the inherited accessor returns `this`.
10289    let species = match get_property(&ctor, "@@species") {
10290        Ok(Value::Undef) => ctor,
10291        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10292        Ok(s) => s,
10293        Err(_) => ctor,
10294    };
10295    if !matches!(
10296        with_host(|h| h.kind_of(&species)),
10297        Some(ObjKind::Class) | Some(ObjKind::Func)
10298    ) {
10299        return Ok(plain());
10300    }
10301    let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10302    // The constructor is called with the LENGTH, so the elements are written
10303    // afterwards — which is also what lets a subclass constructor observe the
10304    // allocation, as node's does.
10305    write_elements(&out, items);
10306    Ok(out)
10307}
10308
10309fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10310    array_method_on(recv, recv, name, args)
10311}
10312
10313/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10314/// generic path to copy the result back onto the array-like.
10315const ARRAY_MUTATORS: &[&str] = &[
10316    "push",
10317    "pop",
10318    "shift",
10319    "unshift",
10320    "splice",
10321    "sort",
10322    "reverse",
10323    "fill",
10324    "copyWithin",
10325];
10326
10327/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10328/// a DOM-ish collection, `arguments`).
10329///
10330/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10331/// rather than over an Array's element vector, so the receiver only has to have
10332/// a `length`. The elements are read out into a temporary Array, the ordinary
10333/// implementation runs on that, and a MUTATING method writes the result back —
10334/// which keeps one implementation of each method rather than a second, generic
10335/// one that could drift from it.
10336///
10337/// An index the receiver does not own is a HOLE in the temporary, so the
10338/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10339fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10340    let len = match get_property(recv, "length") {
10341        Ok(v) => host::to_array_length(&v).unwrap_or(0),
10342        Err(_) => 0,
10343    };
10344    // A STRING receiver owns every index of its length; `has_property` answers
10345    // for objects and reports none of them, which made `[].map.call('abc', f)`
10346    // an array of three holes.
10347    let dense = with_host(|h| h.as_str(recv)).is_some();
10348    let mut items = Vec::with_capacity(len);
10349    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10350    for i in 0..len {
10351        let k = i.to_string();
10352        if dense || has_property(recv, &k)? {
10353            items.push(get_property(recv, &k)?);
10354        } else {
10355            holes.insert(i);
10356            items.push(Value::Undef);
10357        }
10358    }
10359    let tmp = with_host(|h| {
10360        let a = h.new_array(items);
10361        h.install_holes(&a, holes);
10362        a
10363    });
10364    let out = array_method_on(&tmp, recv, method, args)?;
10365    if ARRAY_MUTATORS.contains(&method) {
10366        let result = with_host(|h| match h.get(&tmp) {
10367            Some(JsObj::Array(items)) => items.clone(),
10368            _ => Vec::new(),
10369        });
10370        for (i, v) in result.iter().enumerate() {
10371            set_property(recv, &i.to_string(), v.clone())?;
10372        }
10373        set_property(recv, "length", Value::Float(result.len() as f64))?;
10374    }
10375    Ok(out)
10376}
10377
10378/// `Array.prototype.<name>` on `recv`.
10379///
10380/// `this_value` is what a callback receives as its third argument and what a
10381/// mutating method returns — the same object as `recv` for an ordinary array
10382/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10383/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10384fn array_method_on(
10385    recv: &Value,
10386    this_value: &Value,
10387    name: &str,
10388    args: Vec<Value>,
10389) -> Result<Value, String> {
10390    let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10391    match name {
10392        "push" => {
10393            // 23.1.3.23 step 4 defines each new element through
10394            // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10395            // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10396            // TypeError. The elements were appended to the backing vector
10397            // regardless, so sealing an array did not seal it.
10398            if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10399                let at = array_len(recv);
10400                return Err(host::type_error(&format!(
10401                    "Cannot add property {at}, object is not extensible"
10402                )));
10403            }
10404            // Step 5 then SETS `length`, so a non-writable one refuses the push
10405            // too — `defineProperty(a, 'length', {writable: false})` makes an
10406            // array append-proof without sealing it.
10407            if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10408                return Err(host::type_error(
10409                    "Cannot assign to read only property 'length' of object '[object Array]'",
10410                ));
10411            }
10412            // `push` returns the new length; take it from the same mutable
10413            // borrow rather than copying the array back out to count it.
10414            let len = with_host(|h| {
10415                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10416                    items.extend(args.iter().cloned());
10417                    items.len()
10418                } else {
10419                    0
10420                }
10421            });
10422            Ok(Value::Float(len as f64))
10423        }
10424        "pop" => Ok(with_host(|h| {
10425            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10426                items.pop().unwrap_or(Value::Undef)
10427            } else {
10428                Value::Undef
10429            };
10430            let len = match h.get(recv) {
10431                Some(JsObj::Array(items)) => items.len(),
10432                _ => 0,
10433            };
10434            h.truncate_holes(recv, len);
10435            popped
10436        })),
10437        "shift" => Ok(with_host(|h| {
10438            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10439                if items.is_empty() {
10440                    Value::Undef
10441                } else {
10442                    items.remove(0)
10443                }
10444            } else {
10445                Value::Undef
10446            };
10447            h.remap_holes(recv, |i| i.checked_sub(1));
10448            shifted
10449        })),
10450        "unshift" => {
10451            with_host(|h| {
10452                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10453                    for (i, a) in args.iter().enumerate() {
10454                        items.insert(i, a.clone());
10455                    }
10456                }
10457                let n = args.len();
10458                h.remap_holes(recv, |i| Some(i + n));
10459            });
10460            Ok(Value::Float(array_len(recv) as f64))
10461        }
10462        "join" => {
10463            let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10464                ",".to_string()
10465            } else {
10466                arg_to_string(&args, 0)?
10467            };
10468            join_array(recv, &sep)
10469        }
10470        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10471        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10472        // empty string. It threw `is not a function` — the whole method was
10473        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10474        "toLocaleString" => {
10475            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10476            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10477            if !host::join_stack_push(recv) {
10478                return Ok(with_host(|h| h.new_str(String::new())));
10479            }
10480            let items = array_items(recv);
10481            let mut parts: Vec<String> = Vec::with_capacity(items.len());
10482            for it in &items {
10483                if with_host(|h| h.is_nullish(it)) {
10484                    parts.push(String::new());
10485                    continue;
10486                }
10487                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10488                    Ok(v) => v,
10489                    Err(e) => {
10490                        host::join_stack_pop();
10491                        return Err(e);
10492                    }
10493                };
10494                parts.push(with_host(|h| h.str_of(&v)));
10495            }
10496            host::join_stack_pop();
10497            Ok(with_host(|h| h.new_str(parts.join(","))))
10498        }
10499        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10500        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10501        // `Get`-based `includes` reports `true` for the same array.
10502        "indexOf" => {
10503            let target = arg0(&args);
10504            let len = array_len(recv);
10505            let start = search_start(arg_num(&args, 1), len);
10506            let mut idx = None;
10507            for i in start..len {
10508                // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10509                // index a mutation has since dropped — is skipped, not compared.
10510                if index_absent(recv, i) || i >= array_len(recv) {
10511                    continue;
10512                }
10513                let x = get_property(recv, &i.to_string())?;
10514                if with_host(|h| h.strict_eq(&x, &target)) {
10515                    idx = Some(i);
10516                    break;
10517                }
10518            }
10519            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10520        }
10521        "lastIndexOf" => {
10522            let items = array_items(recv);
10523            let holes = absent_set(recv);
10524            let target = arg0(&args);
10525            let from = (args.len() > 1).then(|| arg_num(&args, 1));
10526            let idx = match search_start_last(from, items.len()) {
10527                None => None,
10528                Some(start) => with_host(|h| {
10529                    items[..=start]
10530                        .iter()
10531                        .enumerate()
10532                        .rev()
10533                        .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10534                        .map(|(i, _)| i)
10535                }),
10536            };
10537            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10538        }
10539        "includes" => {
10540            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10541            // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10542            // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10543            let target = arg0(&args);
10544            let tnan = matches!(target, Value::Float(f) if f.is_nan());
10545            let len = array_len(recv);
10546            let start = search_start(arg_num(&args, 1), len);
10547            let mut found = false;
10548            for i in start..len {
10549                let x = array_elem_live(recv, i)?;
10550                if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10551                    || with_host(|h| h.strict_eq(&x, &target))
10552                {
10553                    found = true;
10554                    break;
10555                }
10556            }
10557            Ok(Value::Bool(found))
10558        }
10559        "slice" => {
10560            let items = array_items(recv);
10561            let (lo, hi) = slice_bounds(&args, items.len());
10562            let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10563            with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10564            Ok(out)
10565        }
10566        "concat" => {
10567            let mut out = array_items(recv);
10568            // A hole in either the receiver or a spreadable argument stays a hole
10569            // in the result, at its shifted position.
10570            let mut holes = absent_set(recv);
10571            let mut sources: Vec<(Value, usize)> = Vec::new();
10572            for a in &args {
10573                // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether an
10574                // argument is spread, overriding `IsArray` in BOTH directions:
10575                // a plain array-like opts IN, and an array opts OUT. It was
10576                // never consulted, so an array was always spread and an
10577                // array-like never was.
10578                let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10579                let spread = if matches!(flag, Value::Undef) {
10580                    matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10581                } else {
10582                    with_host(|h| h.truthy(&flag))
10583                };
10584                if !spread {
10585                    out.push(a.clone());
10586                    continue;
10587                }
10588                match with_host(|h| h.get(a).cloned()) {
10589                    // Read off the backing vector rather than through
10590                    // `array_items`, so the resolve that does for the receiver
10591                    // has to be done here too: 23.1.3.1 step 5.c.iv is a
10592                    // `[[Get]]`, and an index with a getter — or an elided one
10593                    // the chain supplies — is stale in that vector.
10594                    Some(JsObj::Array(mut items)) => {
10595                        resolve_index_accessors(a, &mut items);
10596                        sources.push((a.clone(), out.len()));
10597                        out.extend(items);
10598                    }
10599                    // An opted-in array-LIKE spreads by its `length` and index
10600                    // properties rather than by a backing vector it has none of.
10601                    _ => {
10602                        let len = get_property(a, "length").unwrap_or(Value::Undef);
10603                        let n = with_host(|h| h.to_number(&len));
10604                        let n = if n.is_finite() {
10605                            n.max(0.0) as usize
10606                        } else {
10607                            0
10608                        };
10609                        for i in 0..n {
10610                            out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10611                        }
10612                    }
10613                }
10614            }
10615
10616            for (src, base) in sources {
10617                holes.extend(
10618                    with_host(|h| h.hole_indices(&src))
10619                        .into_iter()
10620                        .map(|i| i + base),
10621                );
10622            }
10623            let arr = array_species_create(this_value, out)?;
10624            with_host(|h| h.install_holes(&arr, holes));
10625            Ok(arr)
10626        }
10627        "reverse" => {
10628            let len = array_len(recv);
10629            with_host(|h| {
10630                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10631                    items.reverse();
10632                }
10633                h.remap_holes(recv, |i| Some(len - 1 - i));
10634            });
10635            Ok(this_value.clone())
10636        }
10637        "fill" => {
10638            // fill(value[, start[, end]]) — negative indices count from the end.
10639            let val = arg0(&args);
10640            let len = array_len(recv) as i64;
10641            let norm =
10642                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10643            let start = if args.len() >= 2 {
10644                norm(arg_num(&args, 1) as i64)
10645            } else {
10646                0
10647            };
10648            let end = if args.len() >= 3 {
10649                norm(arg_num(&args, 2) as i64)
10650            } else {
10651                len as usize
10652            };
10653            with_host(|h| {
10654                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10655                    for it in items.iter_mut().take(end).skip(start) {
10656                        *it = val.clone();
10657                    }
10658                }
10659                // Every filled position now holds a real value.
10660                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10661            });
10662            Ok(this_value.clone())
10663        }
10664        "copyWithin" => {
10665            // copyWithin(target, start[, end]) — copy a slice within the array.
10666            let items = array_items(recv);
10667            let len = items.len() as i64;
10668            let norm =
10669                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10670            let target = norm(arg_num(&args, 0) as i64);
10671            let start = if args.len() >= 2 {
10672                norm(arg_num(&args, 1) as i64)
10673            } else {
10674                0
10675            };
10676            let end = if args.len() >= 3 {
10677                norm(arg_num(&args, 2) as i64)
10678            } else {
10679                len as usize
10680            };
10681            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10682            let copied = slice.len();
10683            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10684            // deletes the target when the source has no such property);
10685            // everything outside the written range keeps its own.
10686            let src_holes = absent_set(recv);
10687            with_host(|h| {
10688                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10689                    for (k, v) in slice.into_iter().enumerate() {
10690                        if target + k < a.len() {
10691                            a[target + k] = v;
10692                        }
10693                    }
10694                }
10695                let len = len as usize;
10696                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10697                    .iter()
10698                    .copied()
10699                    .filter(|i| *i < target || *i >= (target + copied).min(len))
10700                    .collect();
10701                for k in 0..copied {
10702                    if target + k < len && src_holes.contains(&(start + k)) {
10703                        holes.insert(target + k);
10704                    }
10705                }
10706                h.install_holes(recv, holes);
10707            });
10708            Ok(this_value.clone())
10709        }
10710        "at" => {
10711            let items = array_items(recv);
10712            let mut i = arg_num(&args, 0) as i64;
10713            if i < 0 {
10714                i += items.len() as i64;
10715            }
10716            Ok(if i >= 0 && (i as usize) < items.len() {
10717                items[i as usize].clone()
10718            } else {
10719                Value::Undef
10720            })
10721        }
10722        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10723        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10724        // twice and yields `[2, <1 empty item>, 6]`.
10725        "map" => {
10726            let holes = absent_set(recv);
10727            let cb = arg0(&args);
10728            // The result keeps the source's LENGTH, so a skipped index still
10729            // occupies a slot; `array_walk` only tells us which ones ran.
10730            let mut out = vec![Value::Undef; array_len(recv)];
10731            array_walk(recv, |i, it| {
10732                let v = host::invoke(
10733                    &cb,
10734                    vec![it, Value::Float(i as f64), this_value.clone()],
10735                    this_arg(&args, 1),
10736                )?;
10737                if i < out.len() {
10738                    out[i] = v;
10739                }
10740                Ok(None::<()>)
10741            })?;
10742            let arr = array_species_create(this_value, out)?;
10743            with_host(|h| h.install_holes(&arr, holes));
10744            Ok(arr)
10745        }
10746        "flatMap" => {
10747            let cb = arg0(&args);
10748            let thisarg = this_arg(&args, 1);
10749            let mut out = Vec::new();
10750            array_walk(recv, |i, v| {
10751                let r = host::invoke(
10752                    &cb,
10753                    vec![v, Value::Float(i as f64), this_value.clone()],
10754                    thisarg.clone(),
10755                )?;
10756                match with_host(|h| h.get(&r).cloned()) {
10757                    Some(JsObj::Array(inner)) => out.extend(inner),
10758                    _ => out.push(r),
10759                }
10760                Ok(None::<()>)
10761            })?;
10762            array_species_create(this_value, out)
10763        }
10764        "filter" => {
10765            let cb = arg0(&args);
10766            let mut out = Vec::new();
10767            array_walk(recv, |i, it| {
10768                let keep = host::invoke(
10769                    &cb,
10770                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10771                    this_arg(&args, 1),
10772                )?;
10773                if with_host(|h| h.truthy(&keep)) {
10774                    out.push(it);
10775                }
10776                Ok(None::<()>)
10777            })?;
10778            array_species_create(this_value, out)
10779        }
10780        "forEach" => {
10781            let cb = arg0(&args);
10782            array_walk(recv, |i, it| {
10783                host::invoke(
10784                    &cb,
10785                    vec![it, Value::Float(i as f64), this_value.clone()],
10786                    this_arg(&args, 1),
10787                )?;
10788                Ok(None::<()>)
10789            })?;
10790            Ok(Value::Undef)
10791        }
10792        "find" => {
10793            let items = array_items(recv);
10794            let cb = arg0(&args);
10795            for (i, it) in items.iter().enumerate() {
10796                let m = host::invoke(
10797                    &cb,
10798                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10799                    this_arg(&args, 1),
10800                )?;
10801                if with_host(|h| h.truthy(&m)) {
10802                    return Ok(it.clone());
10803                }
10804            }
10805            Ok(Value::Undef)
10806        }
10807        "findIndex" => {
10808            let items = array_items(recv);
10809            let cb = arg0(&args);
10810            for (i, it) in items.iter().enumerate() {
10811                let m = host::invoke(
10812                    &cb,
10813                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10814                    this_arg(&args, 1),
10815                )?;
10816                if with_host(|h| h.truthy(&m)) {
10817                    return Ok(Value::Float(i as f64));
10818                }
10819            }
10820            Ok(Value::Float(-1.0))
10821        }
10822        "some" => {
10823            let cb = arg0(&args);
10824            let thisarg = this_arg(&args, 1);
10825            let hit = array_walk(recv, |i, v| {
10826                let m = host::invoke(
10827                    &cb,
10828                    vec![v, Value::Float(i as f64), this_value.clone()],
10829                    thisarg.clone(),
10830                )?;
10831                Ok(with_host(|h| h.truthy(&m)).then_some(()))
10832            })?;
10833            Ok(Value::Bool(hit.is_some()))
10834        }
10835        "every" => {
10836            let cb = arg0(&args);
10837            let failed = array_walk(recv, |i, it| {
10838                let m = host::invoke(
10839                    &cb,
10840                    vec![it, Value::Float(i as f64), this_value.clone()],
10841                    this_arg(&args, 1),
10842                )?;
10843                Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10844            })?;
10845            Ok(Value::Bool(failed.is_none()))
10846        }
10847        "reduce" => {
10848            let items = array_items(recv);
10849            let holes = absent_set(recv);
10850            let cb = arg0(&args);
10851            let acc;
10852            let mut start = 0;
10853            if args.len() >= 2 {
10854                acc = args[1].clone();
10855            } else {
10856                // With no seed the accumulator is the first PRESENT element, so a
10857                // leading run of holes is skipped rather than seeding `undefined`.
10858                match (0..items.len()).find(|i| !holes.contains(i)) {
10859                    Some(i) => {
10860                        acc = items[i].clone();
10861                        start = i + 1;
10862                    }
10863                    None => {
10864                        return Err(host::type_error(
10865                            "Reduce of empty array with no initial value",
10866                        ))
10867                    }
10868                }
10869            }
10870            // Each element is read LIVE at its index, so a callback that
10871            // shrinks the array is observed — the tail is skipped rather than
10872            // folded from a stale snapshot.
10873            let mut cur = acc;
10874            array_walk(recv, |i, it| {
10875                if i < start {
10876                    return Ok(None::<()>);
10877                }
10878                cur = host::invoke(
10879                    &cb,
10880                    vec![
10881                        std::mem::replace(&mut cur, Value::Undef),
10882                        it,
10883                        Value::Float(i as f64),
10884                        this_value.clone(),
10885                    ],
10886                    this_arg(&args, 1),
10887                )?;
10888                Ok(None::<()>)
10889            })?;
10890            Ok(cur)
10891        }
10892        "reduceRight" => {
10893            let cb = arg0(&args);
10894            let n = array_len(recv);
10895            let mut acc;
10896            let mut from = n; // one past the next index to process (walking down)
10897            if args.len() >= 2 {
10898                acc = args[1].clone();
10899            } else {
10900                let holes = absent_set(recv);
10901                match (0..n).rev().find(|i| !holes.contains(i)) {
10902                    Some(k) => {
10903                        acc = get_property(recv, &k.to_string())?;
10904                        from = k;
10905                    }
10906                    None => {
10907                        return Err(host::type_error(
10908                            "Reduce of empty array with no initial value",
10909                        ))
10910                    }
10911                }
10912            }
10913            // `acc` moves into the closure and back out on every step, so it
10914            // lives in an Option the closure can take from and refill.
10915            let mut slot = Some(acc);
10916            array_walk_rev(recv, from, |i, v| {
10917                let prev = slot.take().expect("accumulator is refilled each step");
10918                slot = Some(host::invoke(
10919                    &cb,
10920                    vec![prev, v, Value::Float(i as f64), this_value.clone()],
10921                    None,
10922                )?);
10923                Ok(None::<()>)
10924            })?;
10925            acc = slot.expect("accumulator is refilled each step");
10926            Ok(acc)
10927        }
10928        "findLast" => {
10929            let items = array_items(recv);
10930            let cb = arg0(&args);
10931            for i in (0..items.len()).rev() {
10932                let m = host::invoke(
10933                    &cb,
10934                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
10935                    this_arg(&args, 1),
10936                )?;
10937                if with_host(|h| h.truthy(&m)) {
10938                    return Ok(items[i].clone());
10939                }
10940            }
10941            Ok(Value::Undef)
10942        }
10943        "findLastIndex" => {
10944            let items = array_items(recv);
10945            let cb = arg0(&args);
10946            for i in (0..items.len()).rev() {
10947                let m = host::invoke(
10948                    &cb,
10949                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
10950                    this_arg(&args, 1),
10951                )?;
10952                if with_host(|h| h.truthy(&m)) {
10953                    return Ok(Value::Float(i as f64));
10954                }
10955            }
10956            Ok(Value::Float(-1.0))
10957        }
10958        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
10959        // and the holes are re-created at the tail — `[3,,1].sort()` is
10960        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
10961        "sort" => {
10962            let all = array_items(recv);
10963            let holes = absent_set(recv);
10964            let mut items: Vec<Value> = all
10965                .iter()
10966                .enumerate()
10967                .filter(|(i, _)| !holes.contains(i))
10968                .map(|(_, v)| v.clone())
10969                .collect();
10970            sort_values(&mut items, args.first())?;
10971            let present = items.len();
10972            // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
10973            // captured at step 1: `Set` for each sorted element, then `Delete`
10974            // for the holes that followed them. Replacing the whole backing
10975            // vector instead discarded anything the COMPARATOR appended —
10976            // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
10977            // original length with every pushed element gone.
10978            with_host(|h| {
10979                let len = all.len();
10980                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10981                    if a.len() < len {
10982                        a.resize(len, Value::Undef);
10983                    }
10984                    for (i, v) in items.into_iter().enumerate() {
10985                        a[i] = v;
10986                    }
10987                    for slot in a[present..len].iter_mut() {
10988                        *slot = Value::Undef;
10989                    }
10990                }
10991                h.install_holes(recv, (present..len).collect());
10992            });
10993            Ok(this_value.clone())
10994        }
10995        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
10996        "toSorted" => {
10997            let mut items = array_items(recv);
10998            sort_values(&mut items, args.first())?;
10999            Ok(with_host(|h| h.new_array(items)))
11000        }
11001        "toReversed" => {
11002            let mut items = array_items(recv);
11003            items.reverse();
11004            Ok(with_host(|h| h.new_array(items)))
11005        }
11006        "toSpliced" => {
11007            let mut items = array_items(recv);
11008            let len = items.len();
11009            let start = {
11010                let s = arg_num(&args, 0);
11011                if s < 0.0 {
11012                    ((len as f64 + s).max(0.0)) as usize
11013                } else {
11014                    (s as usize).min(len)
11015                }
11016            };
11017            let delete = if args.len() >= 2 {
11018                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11019            } else if args.is_empty() {
11020                0
11021            } else {
11022                len - start
11023            };
11024            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11025            items.splice(start..start + delete, inserts);
11026            Ok(with_host(|h| h.new_array(items)))
11027        }
11028        "with" => {
11029            let mut items = array_items(recv);
11030            let len = items.len() as i64;
11031            let rel = arg_num(&args, 0) as i64;
11032            let idx = if rel < 0 { len + rel } else { rel };
11033            if idx < 0 || idx >= len {
11034                return Err(host::range_error(&format!("Invalid index : {rel}")));
11035            }
11036            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11037            Ok(with_host(|h| h.new_array(items)))
11038        }
11039        "flat" => {
11040            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11041            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11042            let raw = if args.is_empty() {
11043                1.0
11044            } else {
11045                arg_num(&args, 0)
11046            };
11047            let depth = if raw.is_nan() {
11048                0.0
11049            } else if raw.is_infinite() {
11050                raw
11051            } else {
11052                raw.trunc()
11053            };
11054            let mut out = Vec::new();
11055            flatten_into(recv, depth, &mut out)?;
11056            array_species_create(this_value, out)
11057        }
11058        "keys" => {
11059            let n = array_len(recv);
11060            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
11061            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11062        }
11063        "values" | "@@iterator" => {
11064            let items = array_items(recv);
11065            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11066        }
11067        "entries" => {
11068            let items = array_items(recv);
11069            let pairs: Vec<Value> = items
11070                .into_iter()
11071                .enumerate()
11072                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
11073                .collect();
11074            Ok(with_host(|h| {
11075                h.alloc(JsObj::Iter {
11076                    items: pairs,
11077                    idx: 0,
11078                })
11079            }))
11080        }
11081        "splice" => array_splice(recv, args),
11082        // `Array.prototype.toString` IS `join()` with the default separator
11083        // (23.1.3.36), so it converts each element with `ToString` too — and
11084        // shares its cycle cut, which is the whole reason it must not call
11085        // `join_parts` directly: `ToString` of a nested array lands back here.
11086        "toString" => join_array(recv, ","),
11087        // An Array inherits from `Object.prototype` too, so the methods it does
11088        // not override resolve there. `[].hasOwnProperty` already read back as a
11089        // function through the property path, but CALLING it landed here and
11090        // threw `is not a function`.
11091        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11092        _ => Err(host::type_error(&format!("{name} is not a function"))),
11093    }
11094}
11095
11096/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11097/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11098/// cut, which is not optional here: `ToString` of an element that is itself an
11099/// array re-enters through `toString`, so guarding only `join` left
11100/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11101/// process. On node v26.7.0 that expression is `""`.
11102fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11103    if !host::join_stack_push(recv) {
11104        return Ok(with_host(|h| h.new_str(String::new())));
11105    }
11106    // 23.1.3.18 step 6: the length is captured once, then each element is read
11107    // and STRINGIFIED before the next is read. Both halves are observable —
11108    // a getter or a `toString` that shrinks the array is seen by every later
11109    // element, which a read-all-then-convert pass misses.
11110    let parts = (|| -> Result<Vec<String>, String> {
11111        let len = array_len(recv);
11112        let mut out = Vec::with_capacity(len);
11113        for i in 0..len {
11114            let v = array_elem_live(recv, i)?;
11115            out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11116        }
11117        Ok(out)
11118    })();
11119    host::join_stack_pop();
11120    let s = parts?.join(sep);
11121    Ok(with_host(|h| h.new_str(s)))
11122}
11123
11124/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11125/// `null`/`undefined` element contributes the empty string, every other element
11126/// is `ToString(element)` — which for an object means invoking its `toString`,
11127/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11128/// `"[object Object]"`.
11129///
11130/// The all-primitive array — the overwhelmingly common one — is rendered under
11131/// a single host borrow; only an array actually holding an object pays for the
11132/// re-entrant per-element conversion.
11133fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11134    let fast = with_host(|h| {
11135        items
11136            .iter()
11137            .map(|x| match x {
11138                Value::Undef => Some(String::new()),
11139                _ if h.is_null(x) => Some(String::new()),
11140                // A SYMBOL element is primitive but has no `ToString`, so it must
11141                // fall through to the fallible path and throw there:
11142                // `[Symbol()].join()` is a TypeError on node v26.7.0.
11143                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11144                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11145                _ => None,
11146            })
11147            .collect::<Vec<_>>()
11148    });
11149    if fast.iter().all(Option::is_some) {
11150        return Ok(fast.into_iter().flatten().collect());
11151    }
11152    let mut out = Vec::with_capacity(items.len());
11153    for (x, p) in items.iter().zip(fast) {
11154        match p {
11155            Some(s) => out.push(s),
11156            None => {
11157                let s = host::to_string_value(x)?;
11158                out.push(with_host(|h| h.str_of(&s)));
11159            }
11160        }
11161    }
11162    Ok(out)
11163}
11164
11165/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11166/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11167/// the merge step; default order is by the string form of each element.
11168/// Propagates a comparator error.
11169///
11170/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11171/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11172/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11173/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11174/// number of times it is called is.
11175pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11176    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11177    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11178    // invoke path and reporting the generic `null is not a function`.
11179    let cmp = match cmp {
11180        Some(Value::Undef) => None,
11181        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11182            // V8 renders the offending value with `NoSideEffectsToString`, not
11183            // with `util.inspect`: a string appears bare (`: x`) rather than
11184            // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11185            let shown = no_side_effects_string(v);
11186            return Err(host::type_error(&format!(
11187                "The comparison function must be either a function or undefined: {shown}"
11188            )));
11189        }
11190        other => other,
11191    };
11192    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11193    // comparator — it sorts to the end after the defined values are ordered.
11194    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11195    // node v26.7.0; the insertion sort called the comparator twice, on
11196    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11197    // is `undefined`, so swapping keeps the defined values in input order.
11198    let mut defined = 0;
11199    for i in 0..items.len() {
11200        if !matches!(items[i], Value::Undef) {
11201            items.swap(defined, i);
11202            defined += 1;
11203        }
11204    }
11205    merge_sort(&mut items[..defined], cmp)
11206}
11207
11208/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11209/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11210/// `> 0` and the pair keeps its input order.
11211fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11212    match cmp {
11213        Some(cb) => {
11214            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11215            Ok(with_host(|h| h.to_number(&v)))
11216        }
11217        None => {
11218            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11219            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11220            // from Rust's `String` order off the BMP.
11221            let x = with_host(|h| h.str_of(a));
11222            let y = with_host(|h| h.str_of(b));
11223            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11224                Ok(1.0)
11225            } else {
11226                Ok(-1.0)
11227            }
11228        }
11229    }
11230}
11231
11232/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11233/// array cannot walk the native stack the JS comparator also runs on, and the
11234/// two buffers are swapped each pass instead of copied back.
11235fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11236    let n = items.len();
11237    if n < 2 {
11238        return Ok(());
11239    }
11240    let mut src = items.to_vec();
11241    let mut dst = src.clone();
11242    let mut width = 1;
11243    while width < n {
11244        let mut lo = 0;
11245        while lo < n {
11246            let mid = (lo + width).min(n);
11247            let hi = (lo + 2 * width).min(n);
11248            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11249            lo = hi;
11250        }
11251        std::mem::swap(&mut src, &mut dst);
11252        width *= 2;
11253    }
11254    items.clone_from_slice(&src);
11255    Ok(())
11256}
11257
11258/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11259/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11260/// keeps the two `k:0` entries in input order, as node does.
11261fn merge(
11262    left: &[Value],
11263    right: &[Value],
11264    out: &mut [Value],
11265    cmp: Option<&Value>,
11266) -> Result<(), String> {
11267    let (mut i, mut j, mut k) = (0, 0, 0);
11268    while i < left.len() && j < right.len() {
11269        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11270            out[k] = right[j].clone();
11271            j += 1;
11272        } else {
11273            out[k] = left[i].clone();
11274            i += 1;
11275        }
11276        k += 1;
11277    }
11278    for v in left[i..].iter().chain(&right[j..]) {
11279        out[k] = v.clone();
11280        k += 1;
11281    }
11282    Ok(())
11283}
11284
11285/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11286/// f64 so `Infinity` (full flatten) and finite counts share one path.
11287///
11288/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11289/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11290/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11291/// stack floor the VM does, so the answer is a catchable error rather than the
11292/// `fatal runtime error: stack overflow` abort this used to produce.
11293/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11294/// elements because each level tests `HasProperty` before recursing, so a hole
11295/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11296fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11297    if host::stack_exhausted() {
11298        return Err(host::stack_overflow_error());
11299    }
11300    let items = array_items(src);
11301    let holes = absent_set(src);
11302    for (i, it) in items.into_iter().enumerate() {
11303        if holes.contains(&i) {
11304            continue;
11305        }
11306        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11307        if nested {
11308            flatten_into(&it, depth - 1.0, out)?;
11309        } else {
11310            out.push(it);
11311        }
11312    }
11313    Ok(())
11314}
11315
11316fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11317    let len = array_len(recv);
11318    let start = {
11319        let s = arg_num(&args, 0);
11320        if s < 0.0 {
11321            ((len as f64 + s).max(0.0)) as usize
11322        } else {
11323            (s as usize).min(len)
11324        }
11325    };
11326    let delete = if args.len() >= 2 {
11327        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11328    } else {
11329        len - start
11330    };
11331    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11332    let inserted = inserts.len();
11333    // The receiver's holes shift by (inserted - deleted) past the cut, and the
11334    // ones inside the cut move into the RETURNED array at their offset there.
11335    let holes = hole_set(recv);
11336    let removed = with_host(|h| {
11337        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11338            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11339            removed
11340        } else {
11341            Vec::new()
11342        }
11343    });
11344    let spliced = with_host(|h| {
11345        h.install_holes(
11346            recv,
11347            holes
11348                .iter()
11349                .filter_map(|&i| {
11350                    if i < start {
11351                        Some(i)
11352                    } else if i < start + delete {
11353                        None
11354                    } else {
11355                        Some(i - delete + inserted)
11356                    }
11357                })
11358                .collect(),
11359        );
11360        (removed, holes.clone())
11361    });
11362    // The REMOVED elements come back as an array of the receiver's species
11363    // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11364    let (removed, holes) = spliced;
11365    let out = array_species_create(recv, removed)?;
11366    with_host(|h| {
11367        h.install_holes(
11368            &out,
11369            holes
11370                .iter()
11371                .filter(|&&i| i >= start && i < start + delete)
11372                .map(|&i| i - start)
11373                .collect(),
11374        );
11375    });
11376    Ok(out)
11377}
11378
11379fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11380    let norm = |v: f64| -> usize {
11381        if v < 0.0 {
11382            ((len as f64 + v).max(0.0)) as usize
11383        } else {
11384            (v as usize).min(len)
11385        }
11386    };
11387    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11388        0
11389    } else {
11390        norm(arg_num(args, 0))
11391    };
11392    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11393        len
11394    } else {
11395        norm(arg_num(args, 1))
11396    };
11397    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11398    // never a reversed one: JS `slice` clamps `end` up to `start`.
11399    (lo, hi.max(lo))
11400}
11401
11402/// The argument positions each `String.prototype` method coerces with
11403/// `ToNumber` rather than `ToString`. Everything not listed is a string
11404/// position — which matters only for a SYMBOL argument, the one value both
11405/// conversions refuse, and refuse with different wording.
11406///
11407/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11408/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11409/// pair the other way round (a length then a pad string).
11410const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11411    ("at", &[0]),
11412    ("charAt", &[0]),
11413    ("charCodeAt", &[0]),
11414    ("codePointAt", &[0]),
11415    ("endsWith", &[1]),
11416    ("includes", &[1]),
11417    ("indexOf", &[1]),
11418    ("lastIndexOf", &[1]),
11419    ("padEnd", &[0]),
11420    ("padStart", &[0]),
11421    ("repeat", &[0]),
11422    ("slice", &[0, 1]),
11423    ("split", &[1]),
11424    ("startsWith", &[1]),
11425    ("substr", &[0, 1]),
11426    ("substring", &[0, 1]),
11427];
11428
11429/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11430/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11431/// rendered `Symbol(d)` into the result — silently, which is the shape of
11432/// mistake that makes a symbol key leak into text.
11433fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11434    let numeric = STRING_METHOD_NUMERIC_ARGS
11435        .iter()
11436        .find(|(m, _)| *m == name)
11437        .map(|(_, ps)| *ps)
11438        .unwrap_or(&[]);
11439    for (i, a) in args.iter().enumerate() {
11440        if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11441            let kind = if numeric.contains(&i) {
11442                "number"
11443            } else {
11444                "string"
11445            };
11446            return Err(host::type_error(&format!(
11447                "Cannot convert a Symbol value to a {kind}"
11448            )));
11449        }
11450    }
11451    Ok(())
11452}
11453
11454/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11455/// reads them: a numeric position through `ToNumber`, every other through
11456/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11457/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11458/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11459///
11460/// The positions that must NOT be coerced are the ones with their own protocol:
11461/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11462/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11463/// `replace`/`replaceAll`. Each of those already has a path that handles the
11464/// value as an object, and stringifying it first would take that path away.
11465/// The argument positions each `Array.prototype` method coerces with
11466/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11467/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11468/// are stored as given, and `indexOf`/`includes` compare their first argument
11469/// without converting it.
11470const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11471    ("at", &[0]),
11472    ("copyWithin", &[0, 1, 2]),
11473    ("fill", &[1, 2]),
11474    ("flat", &[0]),
11475    ("includes", &[1]),
11476    ("indexOf", &[1]),
11477    ("lastIndexOf", &[1]),
11478    ("slice", &[0, 1]),
11479    ("splice", &[0, 1]),
11480    ("toSpliced", &[0, 1]),
11481    ("with", &[0]),
11482];
11483
11484/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11485/// number, and is deliberately absent.
11486const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11487    ("toExponential", &[0]),
11488    ("toFixed", &[0]),
11489    ("toPrecision", &[0]),
11490    ("toString", &[0]),
11491];
11492
11493/// Replace the listed argument positions with their `ToNumber` value, running a
11494/// user `valueOf` and propagating a throw from it. Every one of these read the
11495/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11496/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11497/// was a RangeError.
11498/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11499fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11500    let v = args.get(i).cloned().unwrap_or(Value::Undef);
11501    let p = host::to_primitive(&v, "number")?;
11502    Ok(with_host(|h| h.to_number(&p)))
11503}
11504
11505fn coerce_numeric_args(
11506    table: &[(&str, &[usize])],
11507    name: &str,
11508    mut args: Vec<Value>,
11509) -> Result<Vec<Value>, String> {
11510    let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11511        return Ok(args);
11512    };
11513    for &i in *positions {
11514        let Some(a) = args.get(i) else { continue };
11515        if matches!(a, Value::Undef) {
11516            continue;
11517        }
11518        let p = host::to_primitive(a, "number")?;
11519        args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11520    }
11521    Ok(args)
11522}
11523
11524/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11525/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11526/// matches at position 0.
11527fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11528    let src = if matches!(v, Value::Undef) {
11529        String::new()
11530    } else {
11531        with_host(|h| h.str_of(v))
11532    };
11533    let fv = with_host(|h| h.new_str(flags.to_string()));
11534    let sv = with_host(|h| h.new_str(src));
11535    regexp_ctor(&[sv, fv])
11536}
11537
11538fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11539    let numeric = STRING_METHOD_NUMERIC_ARGS
11540        .iter()
11541        .find(|(m, _)| *m == name)
11542        .map(|(_, ps)| *ps)
11543        .unwrap_or(&[]);
11544    let protocol = match name {
11545        "replace" | "replaceAll" => Some("@@replace"),
11546        "split" => Some("@@split"),
11547        "match" => Some("@@match"),
11548        "matchAll" => Some("@@matchAll"),
11549        "search" => Some("@@search"),
11550        // These three do not CONSUME `Symbol.match`, they reject a value that
11551        // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11552        // the object intact so that check still sees one; stringifying first
11553        // turned the TypeError into an ordinary search.
11554        "startsWith" | "endsWith" | "includes" => Some("@@match"),
11555        _ => None,
11556    };
11557    let mut out = Vec::with_capacity(args.len());
11558    for (i, a) in args.into_iter().enumerate() {
11559        if matches!(a, Value::Undef) {
11560            out.push(a);
11561            continue;
11562        }
11563        if numeric.contains(&i) {
11564            let p = host::to_primitive(&a, "number")?;
11565            out.push(Value::Float(with_host(|h| h.to_number(&p))));
11566            continue;
11567        }
11568        // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11569        // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11570        // regexp and coerces like anything else. The consuming protocols use
11571        // `GetMethod`, which additionally requires a callable.
11572        let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11573        let carries = |p: &str| match host::protocol_lookup(&a, p) {
11574            Ok(Some(m)) => {
11575                if is_regexp_like {
11576                    with_host(|h| h.truthy(&m))
11577                } else {
11578                    with_host(|h| host::is_callable(h, &m))
11579                }
11580            }
11581            _ => false,
11582        };
11583        let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11584            || (i == 0 && protocol.is_some_and(carries))
11585            || (i == 1
11586                && matches!(name, "replace" | "replaceAll")
11587                && with_host(|h| host::is_callable(h, &a)));
11588        if exempt {
11589            out.push(a);
11590            continue;
11591        }
11592        out.push(host::to_string_value(&a)?);
11593    }
11594    Ok(out)
11595}
11596
11597fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11598    reject_symbol_args(name, &args)?;
11599    let args = coerce_string_args(name, args)?;
11600    // Every index-bearing method below counts UTF-16 code units, so they all
11601    // work off this one decoding rather than off `s.chars()` (code points),
11602    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11603    let u = crate::utf16::Units::of(s);
11604    match name {
11605        // `for…of` / spread over a string iterates CODE POINTS, not code units:
11606        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11607        // is the one string operation that is specified in chars, so it stays
11608        // on `s.chars()` on purpose — do not "fix" it to match the others.
11609        "@@iterator" => {
11610            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11611            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11612        }
11613        "toUpperCase" => Ok(new_s(s.to_uppercase())),
11614        "toLowerCase" => Ok(new_s(s.to_lowercase())),
11615        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11616        // from the plain forms only for the locale-specific mappings (Turkish
11617        // dotless i, Lithuanian accents); with no locale argument they are the
11618        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11619        // `to_lowercase`. They threw `is not a function` before, so the common
11620        // no-argument call — the only form this runtime can answer, since it
11621        // carries no ICU — failed outright rather than agreeing with node.
11622        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11623        // is `'i'` here and `'ı'` in node.
11624        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11625        // has no locale rendering. Missing it made an ARRAY of strings fail too,
11626        // since `Array.prototype.toLocaleString` invokes it per element.
11627        "toLocaleString" => Ok(new_s(s.to_string())),
11628        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11629        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11630        // Locale comparison (ASCII approximation of ICU collation): primary by
11631        // case-folded order, then lowercase sorts before uppercase at a tie.
11632        "localeCompare" => {
11633            let other = with_host(|h| h.str_of(&arg0(&args)));
11634            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11635            let r = match la.cmp(&lb) {
11636                std::cmp::Ordering::Less => -1.0,
11637                std::cmp::Ordering::Greater => 1.0,
11638                std::cmp::Ordering::Equal => {
11639                    let mut t = 0.0;
11640                    for (ca, cb) in s.chars().zip(other.chars()) {
11641                        if ca != cb {
11642                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11643                            break;
11644                        }
11645                    }
11646                    t
11647                }
11648            };
11649            Ok(Value::Float(r))
11650        }
11651        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11652        //
11653        // This used to return the receiver unchanged and only validate the FORM
11654        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11655        // one code point) and `"Å"` (NFD, two) stayed distinct under
11656        // `.normalize()`, so the standard way to compare Unicode text for
11657        // canonical equivalence silently answered `false`, and `NFKC` never
11658        // folded a compatibility character (`"fi"` stayed one code point instead
11659        // of becoming `"fi"`). The tables come from `unicode-normalization`.
11660        "normalize" => {
11661            use unicode_normalization::UnicodeNormalization;
11662            let form = match args.first() {
11663                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11664                _ => "NFC".to_string(),
11665            };
11666            let out = match form.as_str() {
11667                "NFC" => s.nfc().collect::<String>(),
11668                "NFD" => s.nfd().collect::<String>(),
11669                "NFKC" => s.nfkc().collect::<String>(),
11670                "NFKD" => s.nfkd().collect::<String>(),
11671                _ => {
11672                    return Err(host::range_error(
11673                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11674                    ))
11675                }
11676            };
11677            Ok(new_s(out))
11678        }
11679        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11680        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11681        // value this runtime can hold is well-formed by construction and
11682        // `toWellFormed` has nothing to replace. Both answers are therefore
11683        // exact for every string that survives storage; the one case node
11684        // answers differently is a surrogate half extracted by `charAt`/`slice`,
11685        // which is already `U+FFFD` here — the documented lone-surrogate
11686        // boundary in `utf16`, not a separate gap.
11687        "isWellFormed" => Ok(Value::Bool(true)),
11688        "toWellFormed" => Ok(new_s(s.to_string())),
11689        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11690        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11691        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11692        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11693        "toString" | "valueOf" => Ok(new_s(s.to_string())),
11694        "charAt" => {
11695            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11696            Ok(new_s(at.unwrap_or_default()))
11697        }
11698        "at" => {
11699            let n = arg_num(&args, 0);
11700            // A negative position counts back from the end; `NaN` is 0. An
11701            // infinite position is out of range in either direction.
11702            let i = if n.is_nan() {
11703                Some(0i64)
11704            } else if n.is_finite() {
11705                let i = n.trunc() as i64;
11706                Some(if i < 0 { i + u.len() as i64 } else { i })
11707            } else {
11708                None
11709            };
11710            match i
11711                .and_then(|i| usize::try_from(i).ok())
11712                .and_then(|i| u.unit_str(i))
11713            {
11714                Some(c) => Ok(new_s(c)),
11715                None => Ok(Value::Undef),
11716            }
11717        }
11718        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11719        // astral character, not the character. `codePointAt` looks ahead one
11720        // unit and reports the whole scalar when the pair is well formed. They
11721        // agree everywhere on the BMP, which is why they used to share an arm.
11722        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11723        // `codePointAt` yields `undefined` (measured on node v26.7.0).
11724        "charCodeAt" => {
11725            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11726            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11727        }
11728        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11729            Some(cp) => Ok(Value::Float(f64::from(cp))),
11730            None => Ok(Value::Undef),
11731        },
11732        // The search quartet all honor their optional position argument.
11733        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11734        // parameterCount walks a query string with exactly that call.
11735        "indexOf" => {
11736            let needle = needle_units(&args);
11737            let from = clamp_pos(arg_num(&args, 1), u.len());
11738            Ok(Value::Float(
11739                search_from(u.as_slice(), needle.as_slice(), from)
11740                    .map(|i| i as f64)
11741                    .unwrap_or(-1.0),
11742            ))
11743        }
11744        "lastIndexOf" => {
11745            let needle = needle_units(&args);
11746            // An absent or NaN position means "search the whole string".
11747            let n = arg_num(&args, 1);
11748            let upto = if n.is_nan() {
11749                u.len()
11750            } else {
11751                clamp_pos(n, u.len())
11752            };
11753            Ok(Value::Float(
11754                search_last(u.as_slice(), needle.as_slice(), upto)
11755                    .map(|i| i as f64)
11756                    .unwrap_or(-1.0),
11757            ))
11758        }
11759        // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11760        // argument outright, and `IsRegExp` is what decides — so an object
11761        // advertising `Symbol.match` is rejected too. None of them checked.
11762        "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11763            Err(host::type_error(&format!(
11764                "First argument to String.prototype.{name} must not be a regular expression"
11765            )))
11766        }
11767        "includes" => {
11768            let needle = needle_units(&args);
11769            let from = clamp_pos(arg_num(&args, 1), u.len());
11770            Ok(Value::Bool(
11771                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11772            ))
11773        }
11774        "startsWith" => {
11775            let needle = needle_units(&args);
11776            let from = clamp_pos(arg_num(&args, 1), u.len());
11777            Ok(Value::Bool(
11778                u.as_slice()[from..].starts_with(needle.as_slice()),
11779            ))
11780        }
11781        "endsWith" => {
11782            let needle = needle_units(&args);
11783            // The 2nd argument is where the string is treated as ENDING.
11784            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11785                u.len()
11786            } else {
11787                clamp_pos(arg_num(&args, 1), u.len())
11788            };
11789            Ok(Value::Bool(
11790                u.as_slice()[..end].ends_with(needle.as_slice()),
11791            ))
11792        }
11793        "slice" => {
11794            let (lo, hi) = slice_bounds(&args, u.len());
11795            Ok(new_s(u.slice(lo, hi)))
11796        }
11797        "substring" => {
11798            let mut a = arg_num(&args, 0).max(0.0) as usize;
11799            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11800                u.len()
11801            } else {
11802                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11803            };
11804            a = a.min(u.len());
11805            if a > b {
11806                std::mem::swap(&mut a, &mut b);
11807            }
11808            Ok(new_s(u.slice(a, b)))
11809        }
11810        "substr" => {
11811            // A negative start counts from the end: max(len + start, 0).
11812            let len = u.len() as i64;
11813            let mut start = arg_num(&args, 0) as i64;
11814            if start < 0 {
11815                start = (len + start).max(0);
11816            }
11817            let start = (start as usize).min(u.len());
11818            let count = if args.len() >= 2 {
11819                arg_num(&args, 1).max(0.0) as usize
11820            } else {
11821                u.len()
11822            };
11823            let end = start.saturating_add(count).min(u.len());
11824            Ok(new_s(u.slice(start, end)))
11825        }
11826        "repeat" => {
11827            let n = arg_num(&args, 0);
11828            // `RangeError`, not `TypeError`, and the count is named:
11829            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11830            if n < 0.0 || !n.is_finite() {
11831                return Err(host::range_error(&format!(
11832                    "Invalid count value: {}",
11833                    host::fmt_number(n)
11834                )));
11835            }
11836            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11837            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11838            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11839            // is `RangeError: Invalid string length`.
11840            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11841                return Err(host::invalid_string_length());
11842            }
11843            Ok(new_s(s.repeat(n as usize)))
11844        }
11845        "concat" => {
11846            let mut out = s.to_string();
11847            for a in &args {
11848                out.push_str(&with_host(|h| h.str_of(a)));
11849            }
11850            Ok(new_s(out))
11851        }
11852        "padStart" => Ok(new_s(pad(s, &args, true)?)),
11853        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11854        // Regex-taking string methods: dispatch to the regexp module when the
11855        // argument is a RegExp; otherwise keep the plain-string behavior.
11856        // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11857        // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11858        // though a RegExp does define that method. Delegating first skipped the
11859        // check and silently did a single replacement.
11860        "replaceAll"
11861            if is_regexp_arg(&arg0(&args))
11862                && !with_host(
11863                    |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11864                ) =>
11865        {
11866            Err(host::type_error(
11867                "String.prototype.replaceAll called with a non-global RegExp argument",
11868            ))
11869        }
11870        "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11871            if symbol_protocol(
11872                &arg0(&args),
11873                match name {
11874                    "match" => "@@match",
11875                    "matchAll" => "@@matchAll",
11876                    "search" => "@@search",
11877                    "split" => "@@split",
11878                    _ => "@@replace",
11879                },
11880            )
11881            .is_some() =>
11882        {
11883            let sym = match name {
11884                "match" => "@@match",
11885                "matchAll" => "@@matchAll",
11886                "search" => "@@search",
11887                "split" => "@@split",
11888                _ => "@@replace",
11889            };
11890            let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11891            let sv = with_host(|h| h.new_str(s.to_string()));
11892            let mut rest = vec![sv];
11893            rest.extend(args.iter().skip(1).cloned());
11894            host::invoke(&f, rest, Some(arg0(&args)))
11895        }
11896        // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11897        // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11898        // answered `null` for every string argument, which reads as "no match"
11899        // — the one answer a caller cannot tell from a real failure.
11900        // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11901        "match" => {
11902            let a = arg0(&args);
11903            let re = if is_regexp_arg(&a) {
11904                a
11905            } else {
11906                regexp_from_arg(&a, "")?
11907            };
11908            crate::regexp::str_match(s, &re)
11909        }
11910        "matchAll" => {
11911            let a = arg0(&args);
11912            let re = if is_regexp_arg(&a) {
11913                a
11914            } else {
11915                regexp_from_arg(&a, "g")?
11916            };
11917            crate::regexp::str_match_all(s, &re)
11918        }
11919        "search" => {
11920            if is_regexp_arg(&arg0(&args)) {
11921                crate::regexp::str_search(s, &arg0(&args))
11922            } else {
11923                // 22.1.3.17 builds a RegExp from the argument, so a
11924                // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11925                // 1. The substring approximation this replaces agreed only for
11926                // a literal needle, and answered -1 for an absent argument
11927                // where the empty pattern matches at 0.
11928                let re = regexp_from_arg(&arg0(&args), "")?;
11929                crate::regexp::str_search(s, &re)
11930            }
11931        }
11932        "replace" => {
11933            let pat = arg0(&args);
11934            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11935            if is_regexp_arg(&pat) {
11936                crate::regexp::str_replace_regex(s, &pat, &repl, false)
11937            } else if with_host(|h| host::is_callable(h, &repl)) {
11938                Ok(new_s(replace_str_fn(
11939                    s,
11940                    &with_host(|h| h.str_of(&pat)),
11941                    &repl,
11942                    false,
11943                )?))
11944            } else {
11945                let from = with_host(|h| h.str_of(&pat));
11946                let to = with_host(|h| h.str_of(&repl));
11947                Ok(new_s(replace_str_plain(s, &from, &to, false)))
11948            }
11949        }
11950        "replaceAll" => {
11951            let pat = arg0(&args);
11952            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11953            if is_regexp_arg(&pat) {
11954                // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
11955                // because `replaceAll` cannot honour "all" without `g`. This
11956                // used to replace only the first match and say nothing.
11957                let global = with_host(|h| match h.get(&pat) {
11958                    Some(JsObj::RegExp(r)) => r.global,
11959                    _ => true,
11960                });
11961                if !global {
11962                    return Err(host::type_error(
11963                        "String.prototype.replaceAll called with a non-global RegExp argument",
11964                    ));
11965                }
11966                crate::regexp::str_replace_regex(s, &pat, &repl, true)
11967            } else if with_host(|h| host::is_callable(h, &repl)) {
11968                Ok(new_s(replace_str_fn(
11969                    s,
11970                    &with_host(|h| h.str_of(&pat)),
11971                    &repl,
11972                    true,
11973                )?))
11974            } else {
11975                let from = with_host(|h| h.str_of(&pat));
11976                let to = with_host(|h| h.str_of(&repl));
11977                Ok(new_s(replace_str_plain(s, &from, &to, true)))
11978            }
11979        }
11980        "split" => {
11981            if is_regexp_arg(&arg0(&args)) {
11982                let limit = args
11983                    .get(1)
11984                    .filter(|v| !matches!(v, Value::Undef))
11985                    .map(|v| with_host(|h| h.to_number(v)) as usize);
11986                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
11987            }
11988            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
11989                vec![new_s(s.to_string())]
11990            } else {
11991                let sep = with_host(|h| h.str_of(&args[0]));
11992                if sep.is_empty() {
11993                    // `split('')` yields one element per code UNIT, so an astral
11994                    // character becomes its two surrogate halves.
11995                    (0..u.len())
11996                        .filter_map(|i| u.unit_str(i))
11997                        .map(new_s)
11998                        .collect()
11999                } else {
12000                    s.split(&sep as &str)
12001                        .map(|p| new_s(p.to_string()))
12002                        .collect()
12003                }
12004            };
12005            // Optional limit: keep at most `limit` substrings.
12006            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12007                let n = with_host(|h| h.to_number(lim));
12008                if n.is_finite() && n >= 0.0 {
12009                    parts.truncate(n as usize);
12010                }
12011            }
12012            Ok(with_host(|h| h.new_array(parts)))
12013        }
12014        _ => Err(host::type_error(&format!("{name} is not a function"))),
12015    }
12016}
12017
12018/// GetSubstitution (22.1.3.19) for a STRING search value.
12019///
12020/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12021/// the pattern is a regexp or a plain string, but the string path here did a
12022/// raw `str::replace` and passed the template through verbatim — so
12023/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12024/// regexp path has always expanded them.
12025///
12026/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12027/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12028/// what node does.
12029fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12030    let chars: Vec<char> = templ.chars().collect();
12031    let mut out = String::new();
12032    let mut i = 0;
12033    while i < chars.len() {
12034        if chars[i] == '$' && i + 1 < chars.len() {
12035            match chars[i + 1] {
12036                '$' => {
12037                    out.push('$');
12038                    i += 2;
12039                    continue;
12040                }
12041                '&' => {
12042                    out.push_str(matched);
12043                    i += 2;
12044                    continue;
12045                }
12046                '`' => {
12047                    out.push_str(&subject[..position]);
12048                    i += 2;
12049                    continue;
12050                }
12051                '\'' => {
12052                    out.push_str(&subject[position + matched.len()..]);
12053                    i += 2;
12054                    continue;
12055                }
12056                _ => {}
12057            }
12058        }
12059        out.push(chars[i]);
12060        i += 1;
12061    }
12062    out
12063}
12064
12065/// `replace`/`replaceAll` with a string pattern and a string replacement,
12066/// expanding each match's `$` patterns against its own position.
12067fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12068    if from.is_empty() && !all {
12069        return format!("{}{s}", substitute_plain(to, "", 0, s));
12070    }
12071    let mut out = String::new();
12072    let mut rest = 0usize;
12073    while let Some(rel) = s[rest..].find(from) {
12074        let at = rest + rel;
12075        out.push_str(&s[rest..at]);
12076        out.push_str(&substitute_plain(to, from, at, s));
12077        rest = at + from.len();
12078        if !all {
12079            break;
12080        }
12081        // An empty pattern matches between every character; step one along so
12082        // the scan terminates.
12083        if from.is_empty() {
12084            if rest >= s.len() {
12085                break;
12086            }
12087            let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12088            out.push_str(&s[rest..rest + step]);
12089            rest += step;
12090        }
12091    }
12092    out.push_str(&s[rest..]);
12093    out
12094}
12095
12096fn new_s(s: String) -> Value {
12097    with_host(|h| h.new_str(s))
12098}
12099
12100/// Where a forward `indexOf`/`includes` search starts, given the optional
12101/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12102/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12103/// or past the end finds nothing, which callers report as `-1` / `false`.
12104pub(crate) fn search_start(n: f64, len: usize) -> usize {
12105    if n.is_nan() {
12106        return 0;
12107    }
12108    let n = n.trunc();
12109    if n >= 0.0 {
12110        if n >= len as f64 {
12111            len
12112        } else {
12113            n as usize
12114        }
12115    } else {
12116        let from_end = len as f64 + n;
12117        if from_end <= 0.0 {
12118            0
12119        } else {
12120            from_end as usize
12121        }
12122    }
12123}
12124
12125/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12126/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12127/// the last element — which is why this takes an `Option` rather than reading
12128/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12129/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12130pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12131    if len == 0 {
12132        return None;
12133    }
12134    let n = match from {
12135        None => return Some(len - 1),
12136        Some(v) if v.is_nan() => 0.0,
12137        Some(v) => v.trunc(),
12138    };
12139    if n >= 0.0 {
12140        Some(if n >= len as f64 { len - 1 } else { n as usize })
12141    } else {
12142        let k = len as f64 + n;
12143        if k < 0.0 {
12144            None
12145        } else {
12146            Some(k as usize)
12147        }
12148    }
12149}
12150
12151/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12152/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12153fn clamp_pos(n: f64, len: usize) -> usize {
12154    if n.is_nan() || n <= 0.0 {
12155        0
12156    } else if n >= len as f64 {
12157        len
12158    } else {
12159        n.trunc() as usize
12160    }
12161}
12162
12163/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12164/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12165/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12166fn unit_pos(n: f64) -> Option<usize> {
12167    if n.is_nan() {
12168        Some(0)
12169    } else if n < 0.0 || !n.is_finite() {
12170        None
12171    } else {
12172        Some(n.trunc() as usize)
12173    }
12174}
12175
12176/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12177/// the needle is compared in the same alphabet the haystack is indexed by.
12178fn needle_units(args: &[Value]) -> crate::utf16::Units {
12179    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12180}
12181
12182/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12183/// needle matches at `from` itself, as JS specifies.
12184fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12185    if needle.is_empty() {
12186        return Some(from.min(hay.len()));
12187    }
12188    if needle.len() > hay.len() {
12189        return None;
12190    }
12191    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12192}
12193
12194/// The highest index `<= upto` at which `needle` occurs in `hay`.
12195fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12196    if needle.is_empty() {
12197        return Some(upto.min(hay.len()));
12198    }
12199    if needle.len() > hay.len() {
12200        return None;
12201    }
12202    let last = hay.len() - needle.len();
12203    (0..=upto.min(last))
12204        .rev()
12205        .find(|&i| &hay[i..i + needle.len()] == needle)
12206}
12207
12208fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12209    let target_f = arg_num(args, 0);
12210    let target = if target_f.is_finite() && target_f > 0.0 {
12211        target_f as usize
12212    } else {
12213        0
12214    };
12215    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12216    // is `'-𝒳'` in node, not `'--𝒳'`.
12217    let cur = crate::utf16::len(s);
12218    if cur >= target {
12219        return Ok(s.to_string());
12220    }
12221    let filler = if args.len() >= 2 {
12222        with_host(|h| h.str_of(&args[1]))
12223    } else {
12224        " ".to_string()
12225    };
12226    if filler.is_empty() {
12227        return Ok(s.to_string());
12228    }
12229    // Checked only AFTER the two short-circuits, which is the order V8 uses:
12230    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12231    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12232    if target_f > host::MAX_STRING_LENGTH as f64 {
12233        return Err(host::invalid_string_length());
12234    }
12235    let need = target - cur;
12236    let fill = crate::utf16::Units::of(&filler);
12237    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12238    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12239    // (see src/utf16.rs).
12240    let units: Vec<u16> = (0..need)
12241        .filter_map(|i| fill.unit(i % fill.len()))
12242        .collect();
12243    let padding = crate::utf16::to_string_lossy(&units);
12244    Ok(if start {
12245        format!("{padding}{s}")
12246    } else {
12247        format!("{s}{padding}")
12248    })
12249}
12250
12251/// V8's radix rejection, shared by `Number.prototype.toString` and
12252/// `BigInt.prototype.toString` — one string, because they are one message and
12253/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12254/// must be").
12255const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12256
12257/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12258fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12259    match name {
12260        "toString" => {
12261            let radix = match args.first() {
12262                None | Some(Value::Undef) => 10,
12263                Some(_) => {
12264                    let t = arg_num(&args, 0).trunc();
12265                    if !(2.0..=36.0).contains(&t) {
12266                        return Err(host::range_error(RADIX_RANGE));
12267                    }
12268                    t as u32
12269                }
12270            };
12271            Ok(new_s(b.to_str_radix(radix)))
12272        }
12273        // `BigInt.prototype.toLocaleString` groups thousands like the Number
12274        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12275        // returning the bare digits made it the only numeric type that skipped
12276        // grouping. Same en-US-shaped output as `Number.prototype`; the
12277        // `locales`/`options` arguments are ignored (no ICU here).
12278        "toLocaleString" => {
12279            let digits = b.magnitude().to_string();
12280            let sign = if b.sign() == num_bigint::Sign::Minus {
12281                "-"
12282            } else {
12283                ""
12284            };
12285            Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12286        }
12287        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12288        _ => Err(host::type_error(&format!("{name} is not a function"))),
12289    }
12290}
12291
12292fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12293    let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12294    match name {
12295        "toFixed" => {
12296            let digits = arg_num(&args, 0);
12297            if !(0.0..=100.0).contains(&digits.trunc()) {
12298                return Err(host::range_error(
12299                    "toFixed() digits argument must be between 0 and 100",
12300                ));
12301            }
12302            Ok(new_s(to_fixed(n, digits as usize)))
12303        }
12304        "toExponential" => {
12305            // `undefined` (or a missing argument) selects the shortest form.
12306            let f = match args.first() {
12307                None | Some(Value::Undef) => None,
12308                Some(_) => {
12309                    let d = arg_num(&args, 0).trunc();
12310                    if !(0.0..=100.0).contains(&d) {
12311                        return Err(host::range_error(
12312                            "toExponential() argument must be between 0 and 100",
12313                        ));
12314                    }
12315                    Some(d as usize)
12316                }
12317            };
12318            Ok(new_s(to_exponential(n, f)))
12319        }
12320        "toString" => {
12321            // An out-of-range radix THROWS; it does not silently fall back to
12322            // base 10. `(1).toString(37)` returned "1" here, so a support probe
12323            // was told every radix worked.
12324            let radix = match args.first() {
12325                None | Some(Value::Undef) => 10,
12326                Some(_) => {
12327                    let r = arg_num(&args, 0);
12328                    let t = r.trunc();
12329                    if !(2.0..=36.0).contains(&t) {
12330                        return Err(host::range_error(RADIX_RANGE));
12331                    }
12332                    t as u32
12333                }
12334            };
12335            if radix == 10 {
12336                Ok(new_s(host::fmt_number(n)))
12337            } else {
12338                Ok(new_s(to_radix(n, radix)))
12339            }
12340        }
12341        "toPrecision" => {
12342            // `undefined` (or a missing argument) behaves like `toString()`.
12343            match args.first() {
12344                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12345                Some(_) => {
12346                    let p = arg_num(&args, 0).trunc();
12347                    if !(1.0..=100.0).contains(&p) {
12348                        return Err(host::range_error(
12349                            "toPrecision() argument must be between 1 and 100",
12350                        ));
12351                    }
12352                    Ok(new_s(to_precision(n, p as usize)))
12353                }
12354            }
12355        }
12356        "toLocaleString" => Ok(new_s(to_locale_string(n))),
12357        "valueOf" => Ok(Value::Float(n)),
12358        _ => Err(host::type_error(&format!("{name} is not a function"))),
12359    }
12360}
12361
12362/// `Number.prototype.toLocaleString()` with the default locale and options:
12363/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12364/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12365/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12366/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12367/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12368fn to_locale_string(n: f64) -> String {
12369    if n.is_nan() {
12370        return "NaN".to_string();
12371    }
12372    if n.is_infinite() {
12373        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12374    }
12375    let neg = n.is_sign_negative();
12376    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12377    // (and a bare trailing point). `to_fixed` rounds half away from zero.
12378    // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12379    // which is exponential — and the grouping below then chopped up the
12380    // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12381    // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12382    // source: node groups the shortest decimal form, so `(1e100)
12383    // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12384    // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12385    // deliberately different rule — see `bigint_ctor`.)
12386    let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12387    let trimmed = match fixed.split_once('.') {
12388        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12389        None => fixed.as_str(),
12390    };
12391    let (int_part, frac_part) = match trimmed.split_once('.') {
12392        Some((i, f)) => (i, Some(f)),
12393        None => (trimmed, None),
12394    };
12395    let mut out = String::new();
12396    if neg {
12397        out.push('-'); // Intl keeps the sign even for -0.
12398    }
12399    out.push_str(&group_thousands(int_part));
12400    if let Some(f) = frac_part {
12401        out.push('.');
12402        out.push_str(f);
12403    }
12404    out
12405}
12406
12407/// Write a nonnegative decimal string in plain positional form, expanding an
12408/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12409/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12410/// negative exponent (a magnitude below 1, which the caller has already rounded
12411/// to zero), is returned unchanged.
12412fn expand_exponential(s: &str) -> String {
12413    let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12414        return s.to_string();
12415    };
12416    let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12417        return s.to_string();
12418    };
12419    if exp <= 0 {
12420        return s.to_string();
12421    }
12422    let (int_digits, frac_digits) = match mantissa.split_once('.') {
12423        Some((i, f)) => (i.to_string(), f.to_string()),
12424        None => (mantissa.to_string(), String::new()),
12425    };
12426    let mut digits = int_digits;
12427    digits.push_str(&frac_digits);
12428    // The exponent consumes the fractional digits first; whatever is left
12429    // becomes trailing zeros.
12430    let zeros = exp as usize - frac_digits.len().min(exp as usize);
12431    digits.push_str(&"0".repeat(zeros));
12432    digits
12433}
12434
12435/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12436fn group_thousands(int_part: &str) -> String {
12437    let bytes = int_part.as_bytes();
12438    let n = bytes.len();
12439    let mut out = String::with_capacity(n + n / 3);
12440    for (i, &b) in bytes.iter().enumerate() {
12441        if i > 0 && (n - i) % 3 == 0 {
12442            out.push(',');
12443        }
12444        out.push(b as char);
12445    }
12446    out
12447}
12448
12449/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12450/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12451/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12452/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12453///
12454/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12455/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12456/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12457fn to_fixed(n: f64, f: usize) -> String {
12458    if !n.is_finite() {
12459        return host::fmt_number(n);
12460    }
12461    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12462    if n.abs() >= 1e21 {
12463        return host::fmt_number(n);
12464    }
12465    let neg = n < 0.0;
12466    // Exact decimal with guard digits past the rounding position; then round the
12467    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12468    let full = format!("{:.*}", f + 25, n.abs());
12469    let mut body = round_decimal_string(&full, f);
12470    if neg {
12471        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12472    }
12473    body
12474}
12475
12476/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12477/// fractional digits, half away from zero, propagating carry across the point.
12478fn round_decimal_string(s: &str, f: usize) -> String {
12479    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12480    let mut digits: Vec<u8> = int_part
12481        .bytes()
12482        .chain(frac_part.bytes())
12483        .map(|b| b - b'0')
12484        .collect();
12485    let point = int_part.len(); // digits before the decimal point
12486    let keep = point + f; // number of leading digits to keep
12487
12488    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12489    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12490        let mut i = keep;
12491        loop {
12492            if i == 0 {
12493                digits.insert(0, 1);
12494                // A new leading digit shifts the decimal point right by one.
12495                return assemble_decimal(&digits, point + 1, f);
12496            }
12497            i -= 1;
12498            if digits[i] == 9 {
12499                digits[i] = 0;
12500            } else {
12501                digits[i] += 1;
12502                break;
12503            }
12504        }
12505    }
12506    assemble_decimal(&digits, point, f)
12507}
12508
12509/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12510/// `point` digits precede the decimal point.
12511fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12512    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12513    let int_str = int_str.trim_start_matches('0');
12514    let int_str = if int_str.is_empty() { "0" } else { int_str };
12515    if f == 0 {
12516        return int_str.to_string();
12517    }
12518    let frac: String = digits[point..point + f]
12519        .iter()
12520        .map(|d| (d + b'0') as char)
12521        .collect();
12522    format!("{int_str}.{frac}")
12523}
12524
12525/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12526/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12527/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12528/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12529/// exact digits are taken with guard positions and rounded here.
12530fn round_significant(a: f64, p: usize) -> (String, i32) {
12531    let sci = format!("{a:.*e}", p - 1 + 25);
12532    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12533    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12534    let all: Vec<u8> = mant
12535        .chars()
12536        .filter(|c| c.is_ascii_digit())
12537        .map(|c| c as u8 - b'0')
12538        .collect();
12539    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12540    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12541        // Round the p-digit mantissa up, propagating carry; a carry out of the
12542        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12543        let mut d: Vec<u8> = all[..p].to_vec();
12544        let mut i = p;
12545        loop {
12546            if i == 0 {
12547                d.insert(0, 1);
12548                d.truncate(p);
12549                e += 1;
12550                break;
12551            }
12552            i -= 1;
12553            if d[i] == 9 {
12554                d[i] = 0;
12555            } else {
12556                d[i] += 1;
12557                break;
12558            }
12559        }
12560        s = d.iter().map(|x| (x + b'0') as char).collect();
12561    }
12562    (s, e)
12563}
12564
12565/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12566/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12567/// `f` omitted, as many digits as uniquely identify the value are used
12568/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12569/// on the exact value, matching `toPrecision`.
12570fn to_exponential(n: f64, f: Option<usize>) -> String {
12571    if !n.is_finite() {
12572        return host::fmt_number(n);
12573    }
12574    let neg = n < 0.0;
12575    let a = n.abs();
12576    let (s, e) = if a == 0.0 {
12577        // Zero has no significant digits: emit "0" padded to the requested width.
12578        ("0".repeat(f.unwrap_or(0) + 1), 0)
12579    } else {
12580        match f {
12581            Some(f) => round_significant(a, f + 1),
12582            None => {
12583                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12584                let sci = format!("{a:e}");
12585                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12586                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12587                let trimmed = digits.trim_end_matches('0');
12588                let digits = if trimmed.is_empty() { "0" } else { trimmed };
12589                (digits.to_string(), exp_str.parse().unwrap_or(0))
12590            }
12591        }
12592    };
12593    let sign = if e >= 0 { '+' } else { '-' };
12594    let mag = e.abs();
12595    let body = if s.len() == 1 {
12596        format!("{s}e{sign}{mag}")
12597    } else {
12598        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12599    };
12600    if neg {
12601        format!("-{body}")
12602    } else {
12603        body
12604    }
12605}
12606
12607/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12608/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12609/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12610/// retained (`(100).toPrecision(5) === "100.00"`).
12611fn to_precision(n: f64, p: usize) -> String {
12612    if !n.is_finite() {
12613        return host::fmt_number(n);
12614    }
12615    if n == 0.0 {
12616        return if p == 1 {
12617            "0".into()
12618        } else {
12619            format!("0.{}", "0".repeat(p - 1))
12620        };
12621    }
12622    let neg = n < 0.0;
12623    let (s, e) = round_significant(n.abs(), p);
12624    let pp = p as i32;
12625
12626    let body = if e < -6 || e >= pp {
12627        // Exponential: first digit, optional '.rest', signed exponent.
12628        let sign = if e >= 0 { '+' } else { '-' };
12629        let mag = e.abs();
12630        if p == 1 {
12631            format!("{s}e{sign}{mag}")
12632        } else {
12633            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12634        }
12635    } else if e >= 0 {
12636        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12637        let ip = (e + 1) as usize;
12638        if ip == p {
12639            s
12640        } else {
12641            format!("{}.{}", &s[..ip], &s[ip..])
12642        }
12643    } else {
12644        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12645        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12646    };
12647    if neg {
12648        format!("-{body}")
12649    } else {
12650        body
12651    }
12652}
12653
12654/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12655/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12656/// is emitted exact, and fractional digits are produced up to the input double's
12657/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12658/// carry-over back into already-written digits (and into the integer part).
12659fn to_radix(n: f64, radix: u32) -> String {
12660    if !n.is_finite() {
12661        return host::fmt_number(n);
12662    }
12663    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12664    let rf = radix as f64;
12665    let neg = n < 0.0;
12666    let value = n.abs();
12667
12668    let mut integer = value.floor();
12669    let mut fraction = value - integer;
12670
12671    // Fraction digits, most-significant first.
12672    let mut frac: Vec<u8> = Vec::new();
12673    // Only compute fractional digits down to the input double's precision.
12674    let mut delta = 0.5 * (next_up(value) - value);
12675    delta = delta.max(next_up(0.0));
12676    if fraction >= delta {
12677        loop {
12678            // Shift up by one digit.
12679            fraction *= rf;
12680            delta *= rf;
12681            let digit = fraction as usize;
12682            frac.push(digits[digit]);
12683            fraction -= digit as f64;
12684            // Round to even.
12685            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12686                // Carry-over: back-trace already-written fraction digits.
12687                loop {
12688                    match frac.pop() {
12689                        None => {
12690                            // Carried past the point into the integer part.
12691                            integer += 1.0;
12692                            break;
12693                        }
12694                        Some(c) => {
12695                            let d = if c > b'9' {
12696                                (c - b'a' + 10) as u32
12697                            } else {
12698                                (c - b'0') as u32
12699                            };
12700                            if d + 1 < radix {
12701                                frac.push(digits[(d + 1) as usize]);
12702                                break;
12703                            }
12704                            // digit was radix-1: drop it and keep carrying.
12705                        }
12706                    }
12707                }
12708                break;
12709            }
12710            if fraction < delta {
12711                break;
12712            }
12713        }
12714    }
12715
12716    // Integer digits, least-significant first (reversed at the end).
12717    let mut int_out: Vec<u8> = Vec::new();
12718    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12719    while v8_exponent(integer / rf) > 0 {
12720        integer /= rf;
12721        int_out.push(b'0');
12722    }
12723    loop {
12724        let remainder = integer % rf;
12725        int_out.push(digits[remainder as usize]);
12726        integer = (integer - remainder) / rf;
12727        if integer <= 0.0 {
12728            break;
12729        }
12730    }
12731    int_out.reverse();
12732
12733    let mut out: Vec<u8> = Vec::new();
12734    if neg {
12735        out.push(b'-');
12736    }
12737    out.extend_from_slice(&int_out);
12738    if !frac.is_empty() {
12739        out.push(b'.');
12740        out.extend_from_slice(&frac);
12741    }
12742    String::from_utf8(out).unwrap()
12743}
12744
12745/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12746fn next_up(x: f64) -> f64 {
12747    f64::from_bits(x.to_bits() + 1)
12748}
12749
12750/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12751/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12752fn v8_exponent(x: f64) -> i32 {
12753    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12754    if biased == 0 {
12755        -1074 // denormal
12756    } else {
12757        biased - 1075
12758    }
12759}
12760
12761// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12762
12763/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12764/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12765/// but the value kept alongside it is what iteration and `console.log` report,
12766/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12767fn normalize_zero_key(v: Value) -> Value {
12768    match v {
12769        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12770        other => other,
12771    }
12772}
12773
12774fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12775    match name {
12776        "get" => {
12777            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12778            Ok(with_host(|h| match h.get(recv) {
12779                Some(JsObj::Map { entries, .. }) => entries
12780                    .get(&key)
12781                    .map(|(_, v)| v.clone())
12782                    .unwrap_or(Value::Undef),
12783                _ => Value::Undef,
12784            }))
12785        }
12786        "set" => {
12787            let kv = normalize_zero_key(arg0(&args));
12788            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12789            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12790            let key = with_host(|h| host::map_key(h, &kv));
12791            with_host(|h| {
12792                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12793                    entries.insert(key, (kv, vv));
12794                }
12795            });
12796            Ok(recv.clone())
12797        }
12798        "has" => {
12799            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12800            Ok(Value::Bool(with_host(
12801                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12802            )))
12803        }
12804        "delete" => {
12805            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12806            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12807                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12808                _ => false,
12809            })))
12810        }
12811        "clear" => {
12812            with_host(|h| {
12813                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12814                    entries.clear();
12815                }
12816            });
12817            Ok(Value::Undef)
12818        }
12819        "forEach" => {
12820            let cb = arg0(&args);
12821            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12822                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12823                _ => Vec::new(),
12824            });
12825            for (k, v) in pairs {
12826                host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12827            }
12828            Ok(Value::Undef)
12829        }
12830        // LIVE, not a snapshot: an entry added during iteration is visited and
12831        // one deleted before it is reached is not.
12832        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12833            recv,
12834            if name == "@@iterator" {
12835                "entries"
12836            } else {
12837                name
12838            },
12839        )),
12840        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12841    }
12842}
12843
12844/// A weak collection can only hold objects (and unregistered symbols) — a
12845/// primitive key is a `TypeError`, which is how packages probe for weak support.
12846fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12847    let weak = with_host(|h| {
12848        matches!(
12849            h.get(recv),
12850            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12851        )
12852    });
12853    if !weak {
12854        return Ok(());
12855    }
12856    let is_object = with_host(|h| match key {
12857        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12858        _ => false,
12859    });
12860    if is_object {
12861        return Ok(());
12862    }
12863    Err(host::type_error(if kind == "WeakMap" {
12864        "Invalid value used as weak map key"
12865    } else {
12866        "Invalid value used in weak set"
12867    }))
12868}
12869
12870/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12871///
12872/// The seven set operations do NOT require a real `Set` on the right-hand side:
12873/// anything with a numeric `size` and callable `has`/`keys` participates, which
12874/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12875/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12876/// diagnostic, so a bad operand reports which field was wrong rather than
12877/// failing later inside the iteration.
12878struct SetRecord {
12879    obj: Value,
12880    /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12881    /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12882    /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12883    size: f64,
12884    has: Value,
12885    keys: Value,
12886}
12887
12888fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12889    if !with_host(|h| is_object_like(h, other)) {
12890        return Err(host::type_error(&format!(
12891            "Set.prototype.{method} argument must be an object"
12892        )));
12893    }
12894    let raw = get_property(other, "size")?;
12895    let num = host::to_number_value(&raw)?;
12896    if num.is_nan() {
12897        return Err(host::type_error("The .size property is NaN"));
12898    }
12899    let size = num.trunc();
12900    if size < 0.0 {
12901        return Err(host::range_error(&format!("'{size}' is an invalid size")));
12902    }
12903    let has = get_property(other, "has")?;
12904    if !with_host(|h| host::is_callable(h, &has)) {
12905        return Err(host::type_error("string \"has\" is not a function"));
12906    }
12907    let keys = get_property(other, "keys")?;
12908    if !with_host(|h| host::is_callable(h, &keys)) {
12909        return Err(host::type_error("string \"keys\" is not a function"));
12910    }
12911    Ok(SetRecord {
12912        obj: other.clone(),
12913        size,
12914        has,
12915        keys,
12916    })
12917}
12918
12919impl SetRecord {
12920    /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12921    /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12922    fn has(&self, v: &Value) -> Result<bool, String> {
12923        let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12924        Ok(with_host(|h| h.truthy(&r)))
12925    }
12926
12927    /// The operand's elements, drained from the iterator its `keys` method
12928    /// returns. A non-object result is the spec's `Result of the keys method is
12929    /// not an object`, reported before anything is iterated.
12930    fn keys(&self) -> Result<Vec<Value>, String> {
12931        let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
12932        if !with_host(|h| is_object_like(h, &it)) {
12933            return Err(host::type_error(
12934                "Result of the keys method is not an object",
12935            ));
12936        }
12937        host::drain_iterator(&it)
12938    }
12939}
12940
12941/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
12942/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
12943/// LEFT even though it can on the right.
12944fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
12945    if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
12946        return Ok(());
12947    }
12948    Err(host::type_error(&format!(
12949        "Method Set.prototype.{method} called on incompatible receiver {}",
12950        with_host(|h| object_tag(h, recv))
12951    )))
12952}
12953
12954/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
12955///
12956/// Every one of these operations copies `[[SetData]]` *after* it has touched
12957/// the operand — `union` and `symmetricDifference` call the operand's `keys`
12958/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
12959/// result. Snapshotting the receiver up front instead dropped such an element:
12960/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
12961fn set_values(recv: &Value) -> Vec<Value> {
12962    with_host(|h| match h.get(recv) {
12963        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
12964        _ => Vec::new(),
12965    })
12966}
12967
12968fn set_size(recv: &Value) -> f64 {
12969    with_host(|h| match h.get(recv) {
12970        Some(JsObj::Set { entries, .. }) => entries.len() as f64,
12971        _ => 0.0,
12972    })
12973}
12974
12975/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
12976/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
12977fn new_set(items: Vec<Value>) -> Result<Value, String> {
12978    let s = with_host(|h| {
12979        h.alloc(JsObj::Set {
12980            entries: IndexMap::new(),
12981            weak: false,
12982        })
12983    });
12984    for v in items {
12985        set_method(&s, "add", vec![v])?;
12986    }
12987    Ok(s)
12988}
12989
12990fn set_contains(s: &Value, v: &Value) -> bool {
12991    let key = with_host(|h| host::map_key(h, v));
12992    with_host(
12993        |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
12994    )
12995}
12996
12997/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
12998///
12999/// Each one branches on the two sizes and iterates the SMALLER side — not an
13000/// optimization but observable behaviour: which side is walked decides the
13001/// result's order and whether the operand's `has` or its `keys` is the method
13002/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13003/// yields the operand's order, and its `keys` (never its `has`) is called.
13004fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13005    require_set_receiver(recv, name)?;
13006    let other = get_set_record(&arg0(&args), name)?;
13007    let my_size = set_size(recv);
13008    match name {
13009        "union" => {
13010            let keys = other.keys()?;
13011            let mut out = set_values(recv);
13012            out.extend(keys);
13013            new_set(out)
13014        }
13015        "intersection" => {
13016            let mut out = Vec::new();
13017            if my_size <= other.size {
13018                for v in set_values(recv) {
13019                    if other.has(&v)? {
13020                        out.push(v);
13021                    }
13022                }
13023            } else {
13024                for k in other.keys()? {
13025                    if set_contains(recv, &k) {
13026                        out.push(k);
13027                    }
13028                }
13029            }
13030            new_set(out)
13031        }
13032        "difference" => {
13033            if my_size <= other.size {
13034                let mut out = Vec::new();
13035                for v in set_values(recv) {
13036                    if !other.has(&v)? {
13037                        out.push(v);
13038                    }
13039                }
13040                return new_set(out);
13041            }
13042            let out = new_set(set_values(recv))?;
13043            for k in other.keys()? {
13044                set_method(&out, "delete", vec![k])?;
13045            }
13046            Ok(out)
13047        }
13048        "symmetricDifference" => {
13049            // The operand is drained FIRST — the spec takes the iterator before
13050            // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13051            // reflected in the result.
13052            let keys = other.keys()?;
13053            let out = new_set(set_values(recv))?;
13054            for k in keys {
13055                if set_contains(recv, &k) {
13056                    set_method(&out, "delete", vec![k])?;
13057                } else {
13058                    set_method(&out, "add", vec![k])?;
13059                }
13060            }
13061            Ok(out)
13062        }
13063        "isSubsetOf" => {
13064            if my_size > other.size {
13065                return Ok(Value::Bool(false));
13066            }
13067            for v in set_values(recv) {
13068                if !other.has(&v)? {
13069                    return Ok(Value::Bool(false));
13070                }
13071            }
13072            Ok(Value::Bool(true))
13073        }
13074        "isSupersetOf" => {
13075            if my_size < other.size {
13076                return Ok(Value::Bool(false));
13077            }
13078            for k in other.keys()? {
13079                if !set_contains(recv, &k) {
13080                    return Ok(Value::Bool(false));
13081                }
13082            }
13083            Ok(Value::Bool(true))
13084        }
13085        "isDisjointFrom" => {
13086            if my_size <= other.size {
13087                for v in set_values(recv) {
13088                    if other.has(&v)? {
13089                        return Ok(Value::Bool(false));
13090                    }
13091                }
13092            } else {
13093                for k in other.keys()? {
13094                    if set_contains(recv, &k) {
13095                        return Ok(Value::Bool(false));
13096                    }
13097                }
13098            }
13099            Ok(Value::Bool(true))
13100        }
13101        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13102    }
13103}
13104
13105fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13106    match name {
13107        "add" => {
13108            let vv = normalize_zero_key(arg0(&args));
13109            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13110            let key = with_host(|h| host::map_key(h, &vv));
13111            with_host(|h| {
13112                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13113                    entries.insert(key, vv);
13114                }
13115            });
13116            Ok(recv.clone())
13117        }
13118        "has" => {
13119            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13120            Ok(Value::Bool(with_host(
13121                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13122            )))
13123        }
13124        "delete" => {
13125            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13126            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13127                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13128                _ => false,
13129            })))
13130        }
13131        "clear" => {
13132            with_host(|h| {
13133                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13134                    entries.clear();
13135                }
13136            });
13137            Ok(Value::Undef)
13138        }
13139        "forEach" => {
13140            let cb = arg0(&args);
13141            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13142                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13143                _ => Vec::new(),
13144            });
13145            for v in vals {
13146                host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13147            }
13148            Ok(Value::Undef)
13149        }
13150        "union"
13151        | "intersection"
13152        | "difference"
13153        | "symmetricDifference"
13154        | "isSubsetOf"
13155        | "isSupersetOf"
13156        | "isDisjointFrom" => set_operation(recv, name, args),
13157        // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13158        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13159            recv,
13160            if name == "entries" {
13161                "entries"
13162            } else {
13163                "values"
13164            },
13165        )),
13166        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13167    }
13168}
13169
13170fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13171    // A generator IS its own iterator: both symbol forms return the receiver.
13172    if matches!(name, "@@iterator" | "@@asyncIterator") {
13173        return Ok(recv.clone());
13174    }
13175    // An `async function*` object's methods return PROMISES of the record, and
13176    // its body has to be driven through the await-aware stepper (a plain
13177    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13178    if host::is_async_generator(recv) {
13179        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13180        // `.return`/`.throw` must wait behind a `.next()` that is still
13181        // suspended on an internal `await`, or that `.next()` would report
13182        // `{done: true}` for a value the body had not yet reached. An uncaught
13183        // `.throw(e)` rejects the returned promise; it does not throw here.
13184        return match name {
13185            "next" => Ok(host::async_gen_enqueue(
13186                recv,
13187                host::GenReq::Next(arg0(&args)),
13188            )),
13189            "return" => Ok(host::async_gen_enqueue(
13190                recv,
13191                host::GenReq::Return(arg0(&args)),
13192            )),
13193            "throw" => Ok(host::async_gen_enqueue(
13194                recv,
13195                host::GenReq::Throw(arg0(&args)),
13196            )),
13197            "@@asyncIterator" => Ok(recv.clone()),
13198            _ => Err(host::type_error(&format!(
13199                "asyncGenerator.{name} is not a function"
13200            ))),
13201        };
13202    }
13203    match name {
13204        "next" => {
13205            let send = arg0(&args);
13206            match host::gen_resume(recv, send)? {
13207                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13208                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13209            }
13210        }
13211        "return" => {
13212            // Resume with an injected return so any pending `finally` runs; the
13213            // completion may itself be a `finally` yield (not-done) or the value.
13214            match host::gen_return(recv, arg0(&args))? {
13215                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13216                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13217            }
13218        }
13219        "throw" => {
13220            // Inject a throw at the suspension point: an enclosing `try/catch` in
13221            // the body can handle it (and any `finally` runs); otherwise it
13222            // propagates to the caller.
13223            match host::gen_throw(recv, arg0(&args))? {
13224                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13225                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13226            }
13227        }
13228        _ => Err(host::type_error(&format!(
13229            "generator.{name} is not a function"
13230        ))),
13231    }
13232}
13233
13234/// A `{ value, done }` iterator-result object.
13235fn iter_result(value: Value, done: bool) -> Value {
13236    with_host(|h| {
13237        let mut m: IndexMap<String, Value> = IndexMap::new();
13238        m.insert("value".into(), value);
13239        m.insert("done".into(), Value::Bool(done));
13240        h.new_object(m)
13241    })
13242}
13243
13244/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
13245/// cursor over a materialized item list.
13246fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13247    match name {
13248        "next" => {
13249            let step = with_host(|h| {
13250                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13251                    if *idx < items.len() {
13252                        let v = items[*idx].clone();
13253                        *idx += 1;
13254                        return Some(v);
13255                    }
13256                }
13257                None
13258            });
13259            Ok(match step {
13260                Some(v) => iter_result(v, false),
13261                None => iter_result(Value::Undef, true),
13262            })
13263        }
13264        "return" => {
13265            // Exhaust the cursor and report done.
13266            with_host(|h| {
13267                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13268                    *idx = items.len();
13269                }
13270            });
13271            Ok(iter_result(arg0(&args), true))
13272        }
13273        // An iterator is its own iterable.
13274        "@@iterator" => Ok(recv.clone()),
13275        _ => Err(host::type_error(&format!(
13276            "iterator.{name} is not a function"
13277        ))),
13278    }
13279}
13280
13281fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13282    match name {
13283        "toString" => Ok(with_host(|h| {
13284            let s = h.str_of(recv);
13285            h.new_str(s)
13286        })),
13287        // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13288        // itself for EVERY hint — it ignores its argument. That is what makes
13289        // `sym + ''` a TypeError rather than a concatenation: the conversion
13290        // succeeds and hands back a symbol, and it is `+` that then rejects it.
13291        "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13292        _ => Err(host::type_error(&format!(
13293            "symbol.{name} is not a function"
13294        ))),
13295    }
13296}
13297
13298// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13299
13300fn object_create(args: Vec<Value>) -> Result<Value, String> {
13301    let proto = arg0(&args);
13302    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13303    // `undefined` is NOT accepted — measured on node v26.7.0,
13304    // `Object.create(undefined)` is
13305    // `TypeError: Object prototype may only be an Object or null: undefined`,
13306    // where node-js quietly built a normal object.
13307    reject_bad_prototype(&proto)?;
13308    let obj = with_host(|h| h.new_object(IndexMap::new()));
13309    // `set_proto` records a null proto as an explicit null-prototype object.
13310    with_host(|h| h.set_proto(&obj, proto));
13311    // Optional second arg: a property-descriptor map.
13312    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13313        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13314            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13315            _ => Vec::new(),
13316        });
13317        for (k, d) in entries {
13318            apply_descriptor(&obj, &k, &d)?;
13319        }
13320    }
13321    Ok(obj)
13322}
13323
13324/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13325/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13326/// `EventEmitter.prototype` (the one express mixes onto its app function).
13327/// The own property names of `<Ctor>.prototype`, and whether each is
13328/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13329///
13330/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13331/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13332/// but were not enumerable, so feature detection that walks a prototype found
13333/// nothing there. The table is read from the reference engine rather than
13334/// derived from the arity table because the arity table holds functions only:
13335/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13336/// `URL.prototype` components are accessors.
13337fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13338    let ctor = ns.strip_suffix(".prototype")?;
13339    crate::arity::PROTO_MEMBERS
13340        .binary_search_by(|(k, _)| (*k).cmp(ctor))
13341        .ok()
13342        .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13343}
13344
13345fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13346    match ns {
13347        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13348        _ => None,
13349    }
13350}
13351
13352/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13353/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13354/// filtered out of); every other receiver answers from its property map.
13355fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13356    if let Some(keys) = crate::proxy::own_keys(v)? {
13357        return Ok(keys
13358            .iter()
13359            .filter(|k| host::is_symbol_key(k))
13360            .map(|k| crate::proxy::key_value(k))
13361            .collect());
13362    }
13363    // An intrinsic prototype's symbol-keyed members come from the generated
13364    // table, which is the only record of them: they own no map entry, so
13365    // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13366    // reports `Symbol.iterator` and `Symbol.unscopables`.
13367    if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13368        if let Some(members) = intrinsic_proto_members(&ns) {
13369            return Ok(with_host(|h| {
13370                members
13371                    .iter()
13372                    .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13373                    .map(|name| h.well_known_symbol(name))
13374                    .collect()
13375            }));
13376        }
13377    }
13378    Ok(with_host(|h| h.own_symbol_keys(v)))
13379}
13380
13381/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13382pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13383    object_define_property(vec![obj.clone(), key, desc])
13384}
13385
13386/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13387pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13388    object_get_own_descriptor(vec![obj.clone(), key])
13389}
13390
13391fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13392    let obj = arg0(&args);
13393    // A Proxy defines through its `defineProperty` trap; the target it forwards
13394    // to is where the ordinary path below finally runs.
13395    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13396        let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13397        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13398        if !with_host(|h| is_object_like(h, &desc)) {
13399            return Err(host::type_error(&format!(
13400                "Property description must be an object: {}",
13401                with_host(|h| h.str_of(&desc))
13402            )));
13403        }
13404        // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13405        // too. `Reflect.defineProperty` is the form that reports `false`.
13406        if !crate::proxy::define_property(&obj, &key, &desc)? {
13407            return Err(host::type_error(&format!(
13408                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13409            )));
13410        }
13411        return Ok(obj);
13412    }
13413    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13414    // target and a non-object descriptor each throw before anything is written.
13415    if !with_host(|h| is_object_like(h, &obj)) {
13416        return Err(host::type_error(
13417            "Object.defineProperty called on non-object",
13418        ));
13419    }
13420    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13421    if !with_host(|h| is_object_like(h, &desc)) {
13422        return Err(host::type_error(&format!(
13423            "Property description must be an object: {}",
13424            with_host(|h| h.str_of(&desc))
13425        )));
13426    }
13427    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13428    apply_descriptor(&obj, &key, &desc)?;
13429    Ok(obj)
13430}
13431
13432/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13433/// for anything else (28.1). A primitive was being accepted and silently
13434/// producing nothing.
13435/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13436/// `Reflect.construct` take.
13437///
13438/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13439/// iterator was being used instead, so an array-like produced nothing and a
13440/// primitive produced nothing rather than the TypeError node raises.
13441/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13442/// object still accepts, because it changes nothing.
13443///
13444/// The observable prototype, not the stored link: an ordinary object has no
13445/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13446/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13447/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13448/// step 8 walks up from `p` looking for `obj`.
13449///
13450/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13451/// built a ring. Nothing hung, because every chain walk in this host carries a
13452/// hop limit, but a lookup then silently gave up instead of finding a property
13453/// that really was there.
13454fn would_cycle(obj: &Value, p: &Value) -> bool {
13455    let mut cur = Some(p.clone());
13456    for _ in 0..1000 {
13457        let Some(c) = cur else { return false };
13458        if with_host(|h| h.strict_eq(&c, obj)) {
13459            return true;
13460        }
13461        // A PROXY's prototype is its handler's business; the spec skips the
13462        // walk entirely when one is in the chain.
13463        if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13464            return false;
13465        }
13466        cur = with_host(|h| h.proto_of(&c));
13467    }
13468    false
13469}
13470
13471fn same_prototype(obj: &Value, p: &Value) -> bool {
13472    let cur = prototype_of(obj);
13473    with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13474}
13475
13476fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13477    if !with_host(|h| is_object_like(h, v)) {
13478        return Err(host::type_error(
13479            "CreateListFromArrayLike called on non-object",
13480        ));
13481    }
13482    let len = get_property(v, "length")?;
13483    let n = with_host(|h| h.to_number(&len));
13484    let n = if n.is_finite() && n > 0.0 {
13485        n as usize
13486    } else {
13487        0
13488    };
13489    (0..n).map(|i| get_property(v, &i.to_string())).collect()
13490}
13491
13492fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13493    if with_host(|h| is_object_like(h, v)) {
13494        return Ok(());
13495    }
13496    Err(host::type_error(&format!(
13497        "Reflect.{method} called on non-object"
13498    )))
13499}
13500
13501/// Whether `v` is an Object in the language sense — anything `typeof` calls
13502/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13503/// distinguish "an object" from a primitive.
13504fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13505    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13506}
13507
13508/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13509/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13510/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13511/// `assign` family: each returned an empty result for `null` where node v26.7.0
13512/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13513/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13514fn require_object_coercible(v: &Value) -> Result<(), String> {
13515    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13516        return Err(host::type_error(
13517            "Cannot convert undefined or null to object",
13518        ));
13519    }
13520    Ok(())
13521}
13522
13523/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13524/// nor `null`, with V8's wording. Measured on node v26.7.0:
13525/// `Object.create("s")` is
13526/// `TypeError: Object prototype may only be an Object or null: s`.
13527fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13528    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13529        return Ok(());
13530    }
13531    Err(host::type_error(&format!(
13532        "Object prototype may only be an Object or null: {}",
13533        with_host(|h| h.str_of(proto))
13534    )))
13535}
13536
13537/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13538///
13539/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13540/// `configurable` field defaults to **false** — which is why a `defineProperty`
13541/// data property is invisible to `Object.keys` unless the caller opts in. That
13542/// asymmetry against plain assignment is the whole reason the attribute table
13543/// exists.
13544/// The requested fields of a property descriptor — 10.1.6.2
13545/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13546/// is the distinction the merge below turns on: an omitted field LEAVES an
13547/// existing attribute alone rather than resetting it.
13548struct Requested {
13549    value: Option<Value>,
13550    get: Option<Option<Value>>,
13551    set: Option<Option<Value>>,
13552    writable: Option<bool>,
13553    enumerable: Option<bool>,
13554    configurable: Option<bool>,
13555}
13556
13557impl Requested {
13558    /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13559    /// `HasProperty`/`Get` pairs do — a descriptor built with
13560    /// `Object.create({ value: 1 })` is legal.
13561    fn read(desc: &Value) -> Self {
13562        let has = |k: &str| {
13563            with_host(|h| {
13564                host::lookup_chain(h, desc, k).is_some()
13565                    || host::lookup_accessor(h, desc, k).is_some()
13566            })
13567        };
13568        let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13569        // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13570        // doing it inside the `with_host` closure aborts on the double borrow.
13571        let flag = |k: &str| {
13572            has(k).then(|| {
13573                let v = val(k);
13574                with_host(|h| h.truthy(&v))
13575            })
13576        };
13577        Requested {
13578            value: has("value").then(|| val("value")),
13579            get: has("get").then(|| match val("get") {
13580                Value::Undef => None,
13581                g => Some(g),
13582            }),
13583            set: has("set").then(|| match val("set") {
13584                Value::Undef => None,
13585                st => Some(st),
13586            }),
13587            writable: flag("writable"),
13588            enumerable: flag("enumerable"),
13589            configurable: flag("configurable"),
13590        }
13591    }
13592
13593    fn is_accessor(&self) -> bool {
13594        self.get.is_some() || self.set.is_some()
13595    }
13596
13597    fn is_data(&self) -> bool {
13598        self.value.is_some() || self.writable.is_some()
13599    }
13600}
13601
13602/// The own property already at `key`, if any, read back through
13603/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13604/// fn-prop side table, Buffer bytes) is covered by one code path.
13605struct Existing {
13606    accessor: bool,
13607    value: Value,
13608    get: Option<Value>,
13609    set: Option<Value>,
13610    writable: bool,
13611    enumerable: bool,
13612    configurable: bool,
13613}
13614
13615fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13616    let k = with_host(|h| h.new_str(key.to_string()));
13617    let d = own_descriptor_pub(obj, k).ok()?;
13618    if matches!(d, Value::Undef) {
13619        return None;
13620    }
13621    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13622    let truthy = |n: &str| {
13623        let v = field(n);
13624        with_host(|h| h.truthy(&v))
13625    };
13626    let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13627    Some(Existing {
13628        accessor,
13629        value: field("value"),
13630        get: match field("get") {
13631            Value::Undef => None,
13632            g => Some(g),
13633        },
13634        set: match field("set") {
13635            Value::Undef => None,
13636            st => Some(st),
13637        },
13638        writable: truthy("writable"),
13639        enumerable: truthy("enumerable"),
13640        configurable: truthy("configurable"),
13641    })
13642}
13643
13644/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13645/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13646/// one with this, not with strict equality.
13647pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13648    let num = |v: &Value| match v {
13649        Value::Int(n) => Some(*n as f64),
13650        Value::Float(f) => Some(*f),
13651        _ => None,
13652    };
13653    match (num(a), num(b)) {
13654        (Some(x), Some(y)) => {
13655            if x.is_nan() && y.is_nan() {
13656                true
13657            } else if x == 0.0 && y == 0.0 {
13658                x.is_sign_negative() == y.is_sign_negative()
13659            } else {
13660                x == y
13661            }
13662        }
13663        _ => with_host(|h| h.strict_eq(a, b)),
13664    }
13665}
13666
13667/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13668///
13669/// None of the validation existed: every `Object.defineProperty` was applied
13670/// unconditionally, so redefining a non-configurable property silently
13671/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13672/// `false` rather than "leave alone", so the ordinary
13673/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13674/// `writable` and `configurable` from a property that had both.
13675///
13676/// Converting an accessor to a data property did not take effect at all: the
13677/// value was written but the accessor stayed in its side table, and accessors
13678/// win on read, so the getter kept answering.
13679fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13680    let req = Requested::read(desc);
13681    let cur = existing_property(obj, key);
13682
13683    // An array's `length` is the exotic own property whose write resizes the
13684    // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13685    // key and left the elements untouched.
13686    if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13687        if let Some(v) = req.value.clone() {
13688            return set_property_pub(obj, "length", v);
13689        }
13690    }
13691
13692    // The other exotics whose own properties are SYNTHESIZED rather than stored
13693    // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13694    // The ordinary path below writes a shadowing map entry the read never
13695    // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13696    // unchanged.
13697    // A builtin namespace/prototype has no property map either, so a data
13698    // descriptor has to reach the same side table an assignment does.
13699    // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13700    // careful polyfill installs itself, precisely to avoid the enumerable
13701    // property a bare assignment creates — wrote a map entry nothing read.
13702    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13703        if let Some(v) = req.value.clone() {
13704            return set_property_pub(obj, key, v);
13705        }
13706    }
13707    let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13708        && key.parse::<usize>().is_ok())
13709        || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13710    if exotic_own {
13711        if let Some(v) = req.value.clone() {
13712            return set_property_pub(obj, key, v);
13713        }
13714    }
13715
13716    // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13717    // object. Only an existing property's attributes were being validated, so
13718    // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13719    if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13720        return Err(host::type_error(&format!(
13721            "Cannot define property {key}, object is not extensible"
13722        )));
13723    }
13724    if let Some(c) = &cur {
13725        if !c.configurable {
13726            let rejected = req.configurable == Some(true)
13727                || req.enumerable.is_some_and(|e| e != c.enumerable)
13728                || (req.is_accessor() && !c.accessor)
13729                || (req.is_data() && c.accessor)
13730                || (c.accessor
13731                    && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13732                        || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13733                || (!c.accessor
13734                    && !c.writable
13735                    && (req.writable == Some(true)
13736                        || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13737            if rejected {
13738                return Err(host::type_error(&format!(
13739                    "Cannot redefine property: {key}"
13740                )));
13741            }
13742        }
13743    }
13744
13745    // An omitted field keeps what the property already had; a brand-new
13746    // property defaults every one of them to false.
13747    let attrs = host::PropAttrs {
13748        writable: req
13749            .writable
13750            .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13751        enumerable: req
13752            .enumerable
13753            .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13754        configurable: req
13755            .configurable
13756            .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13757    };
13758    with_host(|h| h.set_prop_attrs(obj, key, attrs));
13759
13760    if req.is_accessor() {
13761        let get = req
13762            .get
13763            .clone()
13764            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13765        let set = req
13766            .set
13767            .clone()
13768            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13769        // An ACCESSOR at an index past the end still extends the array
13770        // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13771        // `length === 5` with holes between. Only the DATA path grew it, so
13772        // the accessor landed in the side table while `length` stayed put —
13773        // and with it out of range, `Object.keys` and `JSON.stringify` never
13774        // saw the index at all.
13775        if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13776        {
13777            with_host(|h| {
13778                let old_len = match h.get(obj) {
13779                    Some(JsObj::Array(items)) => items.len(),
13780                    _ => 0,
13781                };
13782                if i >= old_len {
13783                    if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13784                        items.resize(i + 1, Value::Undef);
13785                    }
13786                    h.mark_hole_range(obj, old_len..i + 1);
13787                }
13788            });
13789        }
13790        with_host(|h| h.set_accessor(obj, key, get, set));
13791        return Ok(());
13792    }
13793
13794    if let Some(c) = &cur {
13795        if c.accessor {
13796            if !req.is_data() {
13797                // A generic descriptor — flags only — leaves an accessor an
13798                // accessor. They were already applied above.
13799                return Ok(());
13800            }
13801            let v = req.value.clone().unwrap_or(Value::Undef);
13802            with_host(|h| h.accessor_to_data(obj, key, v));
13803            return Ok(());
13804        }
13805    }
13806
13807    let Some(v) = req.value else {
13808        // Nothing to write: a flags-only redefinition of a data property.
13809        return Ok(());
13810    };
13811    write_data_slot(obj, key, v);
13812    Ok(())
13813}
13814
13815/// Store `v` as an own data property, in whichever slot the object kind keeps
13816/// its own properties.
13817fn write_data_slot(obj: &Value, key: &str, v: Value) {
13818    // A function/class receiver stores its own props in the fn-prop side table
13819    // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13820    if matches!(
13821        with_host(|h| h.get(obj).cloned()),
13822        Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13823    ) || uses_side_table(obj)
13824    {
13825        with_host(|h| h.set_fn_prop(obj, key, v));
13826        return;
13827    }
13828    if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13829        // An array's index keys ARE its elements, and defining one past the end
13830        // grows the array with holes in between (10.4.2.1). This whole branch
13831        // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13832        // into the ordinary property map an array does not have, so it was a
13833        // silent no-op.
13834        with_host(|h| {
13835            let old = match h.get(obj) {
13836                Some(JsObj::Array(items)) => items.len(),
13837                _ => 0,
13838            };
13839            if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13840                if i >= old {
13841                    items.resize(i + 1, Value::Undef);
13842                }
13843                items[i] = v;
13844            }
13845            if i > old {
13846                h.mark_hole_range(obj, old..i);
13847            }
13848            h.clear_hole(obj, i);
13849        });
13850        return;
13851    }
13852    with_host(|h| {
13853        if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13854            p.insert(key.to_string(), v);
13855            host::canonicalize_own_keys(p);
13856        }
13857    });
13858}
13859
13860/// `Object.defineProperties(obj, descriptorMap)`.
13861fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
13862    let obj = arg0(&args);
13863    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
13864    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
13865        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13866        _ => Vec::new(),
13867    });
13868    for (k, d) in entries {
13869        apply_descriptor(&obj, &k, &d)?;
13870    }
13871    Ok(obj)
13872}
13873
13874/// The descriptor of an own property a function, a typed array or a RegExp
13875/// SYNTHESIZES rather than keeping in a property map.
13876///
13877/// These read back through the ordinary path but owned no descriptor and did
13878/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
13879/// views of "does this property exist" disagreed — a read said yes while
13880/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
13881/// what a shim checks before patching.
13882fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
13883    let ro_configurable = host::PropAttrs {
13884        writable: false,
13885        enumerable: false,
13886        configurable: true,
13887    };
13888    // A callable's `length`/`name` are read-only but configurable; its
13889    // `prototype` is writable and NOT configurable, and a class's is neither.
13890    // An arrow, a method and a bound function own no `prototype` at all.
13891    if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
13892        return None;
13893    }
13894    if with_host(|h| host::is_callable(h, obj)) {
13895        if key == "prototype" {
13896            let p = get_property(obj, "prototype").ok()?;
13897            if matches!(p, Value::Undef) {
13898                return None;
13899            }
13900            return Some((
13901                p,
13902                host::PropAttrs {
13903                    writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
13904                    enumerable: false,
13905                    configurable: false,
13906                },
13907            ));
13908        }
13909        return Some((get_property(obj, key).ok()?, ro_configurable));
13910    }
13911    // A typed array's elements are own, enumerable, writable, configurable
13912    // properties; an index past the end owns nothing.
13913    if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
13914        let v = crate::stdlib::typedarray::elem_get(obj, key)?;
13915        return Some((
13916            v,
13917            host::PropAttrs {
13918                writable: true,
13919                enumerable: true,
13920                configurable: true,
13921            },
13922        ));
13923    }
13924    // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
13925    if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
13926        return Some((
13927            get_property(obj, "lastIndex").ok()?,
13928            host::PropAttrs {
13929                writable: true,
13930                enumerable: false,
13931                configurable: false,
13932            },
13933        ));
13934    }
13935    None
13936}
13937
13938fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
13939    let obj = arg0(&args);
13940    require_object_coercible(&obj)?;
13941    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13942    // A string primitive's boxed own properties: each code-unit index is an
13943    // enumerable, non-writable, non-configurable data property, and `length` is
13944    // the same minus enumerable.
13945    if let Some(units) = string_primitive_units(&obj) {
13946        let entry = match key.parse::<usize>() {
13947            Ok(i) => units
13948                .get(i)
13949                .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
13950            Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
13951            Err(_) => None,
13952        };
13953        return Ok(match entry {
13954            Some((value, enumerable)) => with_host(|h| {
13955                let mut m: IndexMap<String, Value> = IndexMap::new();
13956                m.insert("value".into(), value);
13957                m.insert("writable".into(), Value::Bool(false));
13958                m.insert("enumerable".into(), Value::Bool(enumerable));
13959                m.insert("configurable".into(), Value::Bool(false));
13960                h.new_object(m)
13961            }),
13962            None => Value::Undef,
13963        });
13964    }
13965    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13966        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
13967    }
13968    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
13969    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
13970    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
13971        if let Some(names) = builtin_proto_method_names(&ns) {
13972            if names.contains(&key.as_str()) {
13973                return Ok(with_host(|h| {
13974                    let thunk = h.alloc(JsObj::Builtin(format!(
13975                        "@proto:{}:{key}",
13976                        ns.trim_end_matches(".prototype")
13977                    )));
13978                    let mut m: IndexMap<String, Value> = IndexMap::new();
13979                    m.insert("value".into(), thunk);
13980                    m.insert("writable".into(), Value::Bool(true));
13981                    m.insert("enumerable".into(), Value::Bool(true));
13982                    m.insert("configurable".into(), Value::Bool(true));
13983                    h.new_object(m)
13984                }));
13985            }
13986        }
13987    }
13988    // A global the object does not own outright is still an own property of the
13989    // global object — the same lazy binding the bare identifier resolves to.
13990    // Every one of them reported `undefined`, so a feature probe written as
13991    // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
13992    // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
13993    // rest are ordinary writable, non-enumerable, configurable bindings.
13994    if with_host(|h| h.is_global_object(&obj)) {
13995        let owned = with_host(|h| match h.get(&obj) {
13996            Some(JsObj::Object(p)) => p.contains_key(&key),
13997            _ => false,
13998        });
13999        if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14000            // A global a SCRIPT created — `x = 1` with no declaration — is an
14001            // ordinary enumerable property, unlike the builtins.
14002            let script_made = with_host(|h| h.read_global(&key).is_some());
14003            if let Some(v) = global_object_binding(&key) {
14004                let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14005                return Ok(with_host(|h| {
14006                    let mut m: IndexMap<String, Value> = IndexMap::new();
14007                    m.insert("value".into(), v);
14008                    m.insert("writable".into(), Value::Bool(!frozen));
14009                    m.insert(
14010                        "enumerable".into(),
14011                        Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14012                    );
14013                    m.insert("configurable".into(), Value::Bool(!frozen));
14014                    h.new_object(m)
14015                }));
14016            }
14017        }
14018    }
14019    // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14020    // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14021    // one of these reads back a value, but none owned a DESCRIPTOR:
14022    // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14023    // as "no such property" to the shim/polyfill family that probes a namespace
14024    // before patching it.
14025    // An ACCESSOR member describes itself with a `get`, never a `value` — and
14026    // it must do so without READING the property, since running the getter
14027    // against the prototype is exactly what throws. Both prototype
14028    // representations are covered, so `Symbol.prototype.description` and
14029    // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14030    // "no such property" to anything that probes before patching.
14031    if let Some(ctor) = intrinsic_proto_of(&obj) {
14032        if is_proto_accessor(&ctor, &key) {
14033            let getter = proto_getter(&ctor, &key);
14034            // The poison pair is the only ECMAScript accessor here with a
14035            // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14036            // `hostname` and the rest are all writable, and reporting them as
14037            // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14038            // 'href').set` read `undefined` for a setter that runs.
14039            let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14040                || crate::stdlib::instance_accessors(&ctor)
14041                    .0
14042                    .iter()
14043                    .any(|(k, settable)| *k == key && *settable);
14044            let setter = writable
14045                .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14046            return Ok(with_host(|h| {
14047                let mut m: IndexMap<String, Value> = IndexMap::new();
14048                m.insert("get".into(), getter);
14049                // `undefined`, not null: a read-only accessor has no setter at
14050                // all, and `JSON.stringify` of the descriptor must drop the key
14051                // rather than report `"set": null`.
14052                m.insert("set".into(), setter.unwrap_or(Value::Undef));
14053                m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14054                m.insert("configurable".into(), Value::Bool(true));
14055                h.new_object(m)
14056            }));
14057        }
14058    }
14059    if let Some(ns) = with_host(|h| match h.get(&obj) {
14060        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14061        _ => None,
14062    }) {
14063        let value = namespace_property(&ns, &key);
14064        if !matches!(value, Value::Undef) {
14065            return Ok(builtin_member_descriptor(&ns, &key, value));
14066        }
14067    }
14068    if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14069        return Ok(with_host(|h| {
14070            let mut m: IndexMap<String, Value> = IndexMap::new();
14071            m.insert("value".into(), value);
14072            m.insert("writable".into(), Value::Bool(attrs.writable));
14073            m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14074            m.insert("configurable".into(), Value::Bool(attrs.configurable));
14075            h.new_object(m)
14076        }));
14077    }
14078    // Accessor descriptor?
14079    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14080        return Ok(with_host(|h| {
14081            let a = h.prop_attrs(&obj, &key);
14082            let mut m: IndexMap<String, Value> = IndexMap::new();
14083            m.insert("get".into(), get.unwrap_or(Value::Undef));
14084            m.insert("set".into(), set.unwrap_or(Value::Undef));
14085            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14086            m.insert("configurable".into(), Value::Bool(a.configurable));
14087            h.new_object(m)
14088        }));
14089    }
14090    let val = with_host(|h| match h.get(&obj) {
14091        // A Buffer's own properties are exactly its byte indices, read out of the
14092        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14093        // that V8 keeps on the prototype, so they own no descriptor.
14094        Some(JsObj::Object(p))
14095            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14096        {
14097            match (
14098                p.get("@@bytes").and_then(|b| h.get(b)),
14099                key.parse::<usize>(),
14100            ) {
14101                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14102                _ => None,
14103            }
14104        }
14105        Some(JsObj::Object(p)) => p.get(&key).cloned(),
14106        // An array's index keys read the elements; `length` is the exotic own
14107        // property; anything else is an ordinary own key in the side table.
14108        Some(JsObj::Array(items)) => match key.parse::<usize>() {
14109            // An ELIDED index owns no property at all, so it has no descriptor.
14110            Ok(i) if h.is_hole(&obj, i) => None,
14111            Ok(i) => items.get(i).cloned(),
14112            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14113            Err(_) => h.fn_prop(&obj, &key),
14114        },
14115        // A function/class own prop lives in the fn-prop side table.
14116        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14117        _ => None,
14118    });
14119    match val {
14120        Some(v) => Ok(with_host(|h| {
14121            let a = h.prop_attrs(&obj, &key);
14122            let mut m: IndexMap<String, Value> = IndexMap::new();
14123            m.insert("value".into(), v);
14124            m.insert("writable".into(), Value::Bool(a.writable));
14125            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14126            m.insert("configurable".into(), Value::Bool(a.configurable));
14127            h.new_object(m)
14128        })),
14129        None => Ok(Value::Undef),
14130    }
14131}
14132
14133/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14134/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14135/// the standard "clone with accessors intact" idiom, so this must agree
14136/// key-for-key with `getOwnPropertyNames`.
14137fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14138    let obj = arg0(&args);
14139    let names = object_keys(vec![obj.clone()], 3)?;
14140    let keys: Vec<String> = with_host(|h| match h.get(&names) {
14141        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14142        _ => Vec::new(),
14143    });
14144    let mut out: IndexMap<String, Value> = IndexMap::new();
14145    for k in keys {
14146        let ks = with_host(|h| h.new_str(k.clone()));
14147        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14148        if !matches!(d, Value::Undef) {
14149            out.insert(k, d);
14150        }
14151    }
14152    Ok(with_host(|h| h.new_object(out)))
14153}
14154
14155/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14156/// Proxy's `has` trap is user code and may throw.
14157pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14158    if let Some(b) = crate::proxy::has(obj, key)? {
14159        return Ok(b);
14160    }
14161    Ok(has_property_ordinary(obj, key))
14162}
14163
14164/// `[[HasProperty]]` for every non-Proxy receiver.
14165fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14166    // `key in globalThis`: membership matches what the READ answers, which for
14167    // the global object includes every lazily-bound builtin and every global a
14168    // script created. `'Math' in globalThis` and `'x' in globalThis` after
14169    // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14170    // read back fine.
14171    if with_host(|h| h.is_global_object(obj))
14172        && !CJS_WRAPPER_LOCALS.contains(&key)
14173        && global_object_binding(key).is_some()
14174    {
14175        return true;
14176    }
14177    // `key in <builtin namespace/prototype>`: membership matches what a property
14178    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14179    // prototype methods) resolve as callable thunks via `namespace_property`, so
14180    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14181    // with the `in` operator before reading the intrinsic).
14182    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14183        return !matches!(namespace_property(&ns, key), Value::Undef);
14184    }
14185    // An integer index of a typed array / Buffer is an own property, and lives
14186    // in the hidden element array rather than the property map — the same
14187    // question `hasOwnProperty` answers, through the same helper. Only a hit
14188    // short-circuits: a non-index key like `'length'` must still fall through
14189    // to the ordinary chain lookup below.
14190    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14191        return true;
14192    }
14193    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14194        return true;
14195    }
14196    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14197        return true;
14198    }
14199    // A member patched onto the receiver's intrinsic prototype. The READ
14200    // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14201    // function while `'at' in []` stayed false.
14202    if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14203        return true;
14204    }
14205    if with_host(|h| match h.get(obj) {
14206        Some(JsObj::Object(p)) => p.contains_key(key),
14207        Some(JsObj::Array(items)) => {
14208            key == "length"
14209                || key
14210                    .parse::<usize>()
14211                    .map(|i| i < items.len() && !h.is_hole(obj, i))
14212                    .unwrap_or(false)
14213                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14214                // side table, and `in` must see it.
14215                || h.fn_prop(obj, key).is_some()
14216        }
14217        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14218        // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14219        // the `RegExpObj` struct rather than a property map, so nothing above
14220        // can see it.
14221        Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14222        _ => false,
14223    }) {
14224        return true;
14225    }
14226    // An INHERITED builtin prototype method. These are not objects on the
14227    // prototype chain — they are synthesized by the read path from the
14228    // intrinsic table — so neither `lookup_chain` nor the property map above
14229    // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14230    // Promise.resolve()` all answered false. That last one is the standard
14231    // thenable test, so the `in` operator disagreed with what a read gives for
14232    // every builtin method of every builtin kind.
14233    inherited_builtin_method(obj, key)
14234}
14235
14236/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14237/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14238/// performs no read, so a getter cannot fire.
14239/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14240/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14241/// `None`.
14242///
14243/// The intrinsic prototypes are namespace handles rather than real objects on
14244/// the chain, so an assignment onto one lands in `builtin_statics` and no
14245/// ordinary chain walk can see it. This is the read side: the receiver's own
14246/// constructor's prototype first, then `Object.prototype`, mirroring
14247/// `inherited_method_owner`'s two-step.
14248pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14249    if with_host(|h| h.has_null_proto(obj)) {
14250        return None;
14251    }
14252    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14253        Some(c) => Some(c),
14254        None if is_arguments(obj) => Some("Object"),
14255        None => with_host(|h| default_ctor_name(h, obj)),
14256    };
14257    // Only the side table is consulted, never the real prototype OBJECT's map:
14258    // `String.prototype` and friends are materialized with their intrinsic
14259    // members present, so reading their maps here would re-route every ordinary
14260    // `"a".toString()` through this path — which recursed until the stack blew.
14261    // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14262    // table precisely so the read side can stay this narrow.
14263    let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14264    let found = ctor.and_then(on).or_else(|| on("Object"))?;
14265    // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14266    // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14267    // name back into the table. Dispatching to it would re-enter this lookup
14268    // and recurse until the stack blew, so a thunk that is already this key's
14269    // own intrinsic reports nothing and the ordinary builtin path answers.
14270    let self_thunk = with_host(
14271        |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14272    );
14273    (!self_thunk).then_some(found)
14274}
14275
14276/// Whether `recv` carries `key` as an OWN property — the guard on
14277/// [`inherited_builtin_static`], since an own property shadows anything
14278/// patched onto a prototype.
14279fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14280    with_host(|h| {
14281        if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14282            return true;
14283        }
14284        match h.get(recv) {
14285            Some(JsObj::Object(p)) => p.contains_key(key),
14286            // An ELIDED index owns nothing — the whole point of a hole is that
14287            // the lookup continues up the chain — so it must not count as a
14288            // shadow here or an inherited value at that index stays invisible.
14289            Some(JsObj::Array(items)) => {
14290                key == "length" || {
14291                    key.parse::<usize>()
14292                        .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14293                }
14294            }
14295            _ => false,
14296        }
14297    })
14298}
14299
14300fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14301    if with_host(|h| h.has_null_proto(obj)) {
14302        return false;
14303    }
14304    if let Some(tag) = crate::stdlib::native_tag(obj) {
14305        if crate::stdlib::instance_has_method(&tag, key) {
14306            return true;
14307        }
14308    }
14309    inherited_method_owner(obj, key).is_some()
14310}
14311
14312/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14313/// whether `Array.prototype`'s methods are still reachable from an array.
14314///
14315/// A builtin's methods are synthesized from the receiver's KIND rather than
14316/// found on a chain, so replacing the prototype could not take them away:
14317/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14318/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14319/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14320/// all still answer, as they do in node.
14321///
14322/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14323/// link at all, so this answers true after one map probe and allocates nothing.
14324pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14325    own_intrinsic_reachable(recv)
14326}
14327
14328fn own_intrinsic_reachable(recv: &Value) -> bool {
14329    // A BOXED primitive needs no special case here: its methods resolve through
14330    // `inherited_method_owner`, which applies the wrapper rule itself.
14331    with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14332}
14333
14334/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14335fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14336    let own = Some(ctor);
14337    let mut cur = recv.clone();
14338    for _ in 0..100 {
14339        let explicit = with_host(|h| h.proto_of(&cur));
14340        let Some(p) = explicit else {
14341            // No explicit link: the implicit prototype is this object's own
14342            // kind's, which is what `recv` is asking about only while `cur` is
14343            // still `recv` itself.
14344            if with_host(|h| h.has_null_proto(&cur)) {
14345                return false;
14346            }
14347            let implicit = with_host(|h| default_ctor_name(h, &cur));
14348            // Every implicit prototype chain ends at `Object.prototype`, so a
14349            // question about `Object` is answered yes by any of them.
14350            return implicit == own || ctor == "Object";
14351        };
14352        if with_host(|h| h.is_null(&p)) {
14353            return false;
14354        }
14355        let hit = with_host(|h| {
14356            own.is_some_and(|c| {
14357                matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14358                    || h.intrinsic_proto_ctor(&p) == Some(c)
14359                    || (c == "Object" && h.object_proto() == p)
14360            })
14361        });
14362        if hit {
14363            return true;
14364        }
14365        // A CLASS prototype object is not linked to the builtin its class
14366        // extends — the `extends` relationship is recorded on the class value —
14367        // so the walk has to cross over there or `class D extends Array {}` ends
14368        // it, and every inherited method of every subclass instance vanishes.
14369        if let Some(builtin) = with_host(|h| {
14370            h.class_owning_proto(&p)
14371                .and_then(|c| h.class_builtin_ancestor(&c))
14372                .map(|b| h.callable_name(&b))
14373        }) {
14374            if own == Some(builtin.as_str()) || ctor == "Object" {
14375                return true;
14376            }
14377        }
14378        cur = p;
14379    }
14380    false
14381}
14382
14383/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14384/// — the complement of [`intrinsic_reachable`], which asks about one known
14385/// constructor.
14386///
14387/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14388/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14389/// it: `o.push(1)` works, because those methods are generic over their receiver
14390/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14391/// worked here). Deciding the owner from the receiver's KIND alone made every
14392/// one of them `undefined` — the same "methods come from the kind, not the
14393/// chain" mistake as the detachment case, in the opposite direction.
14394pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14395    chain_intrinsic_ctors(recv)
14396}
14397
14398fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14399    with_host(|h| chain_intrinsic_ctors_h(h, recv))
14400}
14401
14402/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14403/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14404/// back through `with_host` there aborts the process on a double borrow.
14405pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14406    let mut out: Vec<&'static str> = Vec::new();
14407    let mut cur = recv.clone();
14408    for _ in 0..100 {
14409        let Some(p) = h.proto_of(&cur) else {
14410            break;
14411        };
14412        if h.is_null(&p) {
14413            break;
14414        }
14415        let name = match h.get(&p) {
14416            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14417            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14418        };
14419        if let Some(n) = name {
14420            if let Some(c) = crate::arity::PROTO_MEMBERS
14421                .iter()
14422                .map(|(k, _)| *k)
14423                .find(|k| *k == n)
14424            {
14425                if !out.contains(&c) {
14426                    out.push(c);
14427                }
14428            }
14429        }
14430        cur = p;
14431    }
14432    out
14433}
14434
14435/// The constructor whose prototype defines `key` for `obj` — its own if that
14436/// prototype has it, otherwise `Object` — or `None` when neither does.
14437///
14438/// Used both by `in` and by the READ, so the two cannot disagree about which
14439/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14440/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14441pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14442    inherited_method_owner(obj, key)
14443}
14444
14445fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14446    if with_host(|h| h.has_null_proto(obj)) {
14447        return None;
14448    }
14449    // The generated prototype-member table, which unlike the arity table knows
14450    // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14451    // on a Symbol are members but not functions — and about `constructor`.
14452    // A BOXED primitive reports its wrapper's constructor, not `Object` —
14453    // `'description' in Object(Symbol())` is true. The box is an ordinary
14454    // object carrying the primitive in a slot, so the ctor comes from what it
14455    // holds rather than from the box itself.
14456    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14457        Some(c) => Some(c),
14458        // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14459        // spread and `for-of` work, but node's is an exotic that inherits from
14460        // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14461        // its backing kind would hand it the whole `Array.prototype`.
14462        None if is_arguments(obj) => Some("Object"),
14463        None => with_host(|h| default_ctor_name(h, obj)),
14464    };
14465    let on_proto = |c: &str| {
14466        crate::arity::PROTO_MEMBERS
14467            .binary_search_by(|(k, _)| (*k).cmp(c))
14468            .ok()
14469            .is_some_and(|i| {
14470                crate::arity::PROTO_MEMBERS[i]
14471                    .1
14472                    .iter()
14473                    .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14474            })
14475    };
14476    // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14477    // well-known symbol member is absent from it. For an object whose CHAIN
14478    // reaches an intrinsic prototype the intrinsic table has to be consulted as
14479    // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14480    // at all. It is deliberately NOT consulted for the receiver's own kind:
14481    // there a thunk would be minted for every `@@` member the table names,
14482    // including ones whose dispatch has no implementation for that receiver,
14483    // and `[...buffer]` then failed with `@@iterator is not a function`.
14484    //
14485    // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14486    // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14487    // and minting a thunk there produced `@@iterator is not a function` for
14488    // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14489    // fast path, which the table entry would shadow.
14490    let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14491        && crate::stdlib::native_tag(obj).is_none();
14492    let on_proto_or_symbol =
14493        |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14494    // Each candidate is only an answer while ITS prototype is still on the
14495    // receiver's chain. The two are asked separately: replacing an array's
14496    // prototype with a plain object takes `Array.prototype`'s methods away and
14497    // leaves `Object.prototype`'s, since the replacement inherits from it.
14498    if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14499        return Some(c);
14500    }
14501    // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14502    // kind does not account for.
14503    if let Some(c) = chain_intrinsic_ctors(obj)
14504        .into_iter()
14505        .find(|c| on_proto_or_symbol(c))
14506    {
14507        return Some(c);
14508    }
14509    // Everything else inherits `Object.prototype`'s.
14510    if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14511        return Some("Object");
14512    }
14513    None
14514}
14515
14516/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14517/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14518/// that preserves the *reference graph*. Two properties pointing at the same
14519/// object clone to two properties pointing at the same clone, and a cycle clones
14520/// to a cycle instead of recursing forever. `seen` maps each source heap index
14521/// to its clone, which is what buys both.
14522/// The rendering node puts in a `DataCloneError` for a value the structured
14523/// clone algorithm refuses, or `None` when the value IS cloneable.
14524///
14525/// Refusing at all is the point: these used to be copied through by reference,
14526/// so `structuredClone({f: () => 1})` handed back an object sharing the
14527/// original's function and `structuredClone(new WeakMap())` returned the very
14528/// same WeakMap. Node throws on every one of them.
14529fn clone_refusal(v: &Value) -> Option<String> {
14530    let kind = with_host(|h| h.kind_of(v))?;
14531    let render = |ctor: &str| Some(format!("#<{ctor}>"));
14532    match kind {
14533        // A function renders as its SOURCE TEXT here. This frontend does not
14534        // retain function source (`FuncDef` holds a compiled chunk), so the
14535        // message says `function f() { [code] }` where node quotes the original
14536        // — the error, its name and its code are right, the text is not.
14537        ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14538            Some(with_host(|h| h.str_of(v)))
14539        }
14540        ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14541            Some(with_host(|h| h.str_of(v)))
14542        }
14543        ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14544        ObjKind::Promise => render("Promise"),
14545        ObjKind::Generator => Some("[object Generator]".to_string()),
14546        // A proxy is refused by its TARGET's shape: a callable one renders like
14547        // the function it wraps, everything else as a plain object.
14548        ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14549            with_host(|h| h.str_of(v))
14550        } else {
14551            "#<Object>".to_string()
14552        }),
14553        ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14554            render("WeakMap")
14555        }
14556        ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14557            render("WeakSet")
14558        }
14559        _ => match crate::stdlib::native_tag(v).as_deref() {
14560            Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14561            _ => None,
14562        },
14563    }
14564}
14565
14566/// `structuredClone(value[, { transfer }])`.
14567///
14568/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14569/// after the clone — its bytes belong to the copy. The option used to be
14570/// ignored entirely, so the source buffer stayed usable where node leaves it
14571/// with zero length.
14572fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14573    let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14574        Some(opts) => {
14575            let t = get_property(opts, "transfer")?;
14576            if matches!(t, Value::Undef) {
14577                Vec::new()
14578            } else {
14579                host::iter_all(&t)?
14580            }
14581        }
14582        None => Vec::new(),
14583    };
14584    for item in &list {
14585        if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14586            return Err(host::dom_error(
14587                "DataCloneError",
14588                "Found invalid value in transferList.",
14589            ));
14590        }
14591    }
14592    let out = deep_clone(&arg0(&args))?;
14593    for item in &list {
14594        crate::stdlib::typedarray::detach_buffer(item);
14595    }
14596    Ok(out)
14597}
14598
14599pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14600    deep_clone_seen(v, &mut std::collections::HashMap::new())
14601}
14602
14603fn deep_clone_seen(
14604    v: &Value,
14605    seen: &mut std::collections::HashMap<u32, Value>,
14606) -> Result<Value, String> {
14607    let idx = match v {
14608        Value::Obj(i) => *i,
14609        _ => return Ok(v.clone()),
14610    };
14611    if let Some(done) = seen.get(&idx) {
14612        return Ok(done.clone());
14613    }
14614    if crate::stdlib::typedarray::is_detached(v) {
14615        return Err(host::dom_error(
14616            "DataCloneError",
14617            "An ArrayBuffer is detached and could not be cloned.",
14618        ));
14619    }
14620    if let Some(render) = clone_refusal(v) {
14621        return Err(host::dom_error(
14622            "DataCloneError",
14623            &format!("{render} could not be cloned."),
14624        ));
14625    }
14626    // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14627    // handing back the same object let a write through the clone move the
14628    // original's match cursor.
14629    if let Some((src, flags)) = with_host(|h| match h.get(v) {
14630        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14631        _ => None,
14632    }) {
14633        let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14634        let out = regexp_ctor(&args)?;
14635        seen.insert(idx, out.clone());
14636        return Ok(out);
14637    }
14638    Ok(match with_host(|h| h.get(v).cloned()) {
14639        Some(JsObj::Array(items)) => {
14640            // Register the (empty) clone BEFORE recursing so a self-reference
14641            // resolves to it.
14642            let out = with_host(|h| h.new_array(Vec::new()));
14643            seen.insert(idx, out.clone());
14644            let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14645            for x in &items {
14646                cloned.push(deep_clone_seen(x, seen)?);
14647            }
14648            with_host(|h| {
14649                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14650                    *a = cloned;
14651                }
14652                // A sparse source clones to an equally sparse array: the clone
14653                // walks own properties, so a hole is nothing to copy.
14654                h.copy_holes(v, &out, Some);
14655            });
14656            out
14657        }
14658        Some(JsObj::Object(_)) => {
14659            let out = with_host(|h| h.new_object(IndexMap::new()));
14660            seen.insert(idx, out.clone());
14661            // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14662            // walked the property map, where an accessor stores nothing, so
14663            // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14664            // key and a non-enumerable one are dropped, as node drops them.
14665            let is_error = with_host(|h| h.error_to_string(v)).is_some();
14666            let proto = clone_proto(v);
14667            let keeps_proto = !matches!(proto, CloneProto::Plain);
14668            // An ERROR clones its name, message and stack and NOTHING else —
14669            // node drops any other own property, even an enumerable one.
14670            let keys: Vec<String> = if is_error {
14671                // An ERROR clones its name, message and stack and NOTHING else —
14672                // node drops any other own property, even an enumerable one.
14673                ["name", "message", "stack"]
14674                    .iter()
14675                    .filter(|k| has_property(v, k).unwrap_or(false))
14676                    .map(|k| (*k).to_string())
14677                    .collect()
14678            } else if keeps_proto {
14679                // A preserved exotic keeps EVERY own property, including the
14680                // non-enumerable ones and the internal slots — a Date's time
14681                // value, an ArrayBuffer's `byteLength` and byte store, a typed
14682                // array's view. The enumerable-only walk dropped all of those,
14683                // so a cloned Date read `Invalid Date` and a cloned
14684                // ArrayBuffer had no `byteLength`.
14685                with_host(|h| match h.get(v) {
14686                    Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14687                    _ => Vec::new(),
14688                })
14689            } else {
14690                with_host(|h| h.own_enum_key_names(v))
14691            };
14692            let mut cloned: IndexMap<String, Value> = IndexMap::new();
14693            for k in keys {
14694                // An internal slot is read straight out of the map: it is not a
14695                // property, so a `[[Get]]` would not find it.
14696                let val = if k.starts_with("@@") {
14697                    match with_host(|h| match h.get(v) {
14698                        Some(JsObj::Object(p)) => p.get(&k).cloned(),
14699                        _ => None,
14700                    }) {
14701                        Some(val) => val,
14702                        None => continue,
14703                    }
14704                } else {
14705                    get_property(v, &k)?
14706                };
14707                cloned.insert(k, deep_clone_seen(&val, seen)?);
14708            }
14709            // The prototype survives only for the exotics the algorithm knows —
14710            // a Date, an Error, a typed array, a boxed primitive. A USER class
14711            // instance becomes a plain object, which is what node produces;
14712            // keeping every prototype made `structuredClone(new K())
14713            // instanceof K` true.
14714            with_host(|h| {
14715                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14716                    *p = cloned;
14717                }
14718                match &proto {
14719                    CloneProto::Same => {
14720                        if let Some(p) = h.proto_of(v) {
14721                            h.set_proto(&out, p);
14722                        }
14723                    }
14724                    CloneProto::Ctor(c) => {
14725                        h.ensure_error_protos();
14726                        let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14727                        if let Some(p) = p {
14728                            h.set_proto(&out, p);
14729                        }
14730                        // A Buffer clones to a plain `Uint8Array`, so the native
14731                        // tag has to change with the prototype — left alone,
14732                        // `Buffer.isBuffer` still answered true for the clone.
14733                        // A Buffer clones to a plain `Uint8Array`, so the native
14734                        // tag has to change with the prototype — left alone,
14735                        // `Buffer.isBuffer` answered true for the clone and the
14736                        // brand stayed `[object Object]`. A typed array is
14737                        // tagged `TypedArray` and names its element type in
14738                        // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14739                        if c == "Uint8Array" {
14740                            let tag = h.new_str("TypedArray");
14741                            let kind = h.new_str("Uint8Array");
14742                            if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14743                                p.insert("@@native".into(), tag);
14744                                p.insert("@@kind".into(), kind);
14745                            }
14746                        }
14747                    }
14748                    CloneProto::Plain => {}
14749                }
14750                h.copy_prop_attrs(v, &out);
14751            });
14752            out
14753        }
14754        // Map/Set are structured types: clone the entries, keep the kind.
14755        Some(JsObj::Map { entries, weak }) => {
14756            let out = with_host(|h| {
14757                h.alloc(JsObj::Map {
14758                    entries: IndexMap::new(),
14759                    weak,
14760                })
14761            });
14762            seen.insert(idx, out.clone());
14763            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14764            for (k, val) in pairs {
14765                let ck = deep_clone_seen(&k, seen)?;
14766                let cv = deep_clone_seen(&val, seen)?;
14767                let _ = map_method(&out, "set", vec![ck, cv]);
14768            }
14769            out
14770        }
14771        Some(JsObj::Set { entries, weak }) => {
14772            let out = with_host(|h| {
14773                h.alloc(JsObj::Set {
14774                    entries: IndexMap::new(),
14775                    weak,
14776                })
14777            });
14778            seen.insert(idx, out.clone());
14779            let vals: Vec<Value> = entries.values().cloned().collect();
14780            for x in vals {
14781                let cx = deep_clone_seen(&x, seen)?;
14782                let _ = set_method(&out, "add", vec![cx]);
14783            }
14784            out
14785        }
14786        // A string, a BigInt and a boxed primitive are immutable enough to
14787        // share; anything left is a value type.
14788        _ => v.clone(),
14789    })
14790}
14791
14792/// Whether a cloned object keeps the source's prototype.
14793///
14794/// The structured clone algorithm reproduces the exotics it knows and turns
14795/// everything else into a plain object — so a `Date` clones to a `Date` and a
14796/// user class instance clones to an `Object`.
14797fn clone_proto(v: &Value) -> CloneProto {
14798    // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14799    // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14800    // `Error`, not `E`.
14801    if with_host(|h| h.error_to_string(v)).is_some() {
14802        let name = get_property(v, "name")
14803            .map(|n| with_host(|h| h.str_of(&n)))
14804            .unwrap_or_else(|_| "Error".into());
14805        let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14806            name
14807        } else {
14808            "Error".to_string()
14809        };
14810        return CloneProto::Ctor(class);
14811    }
14812    match crate::stdlib::native_tag(v).as_deref() {
14813        // A Buffer is not reproduced as a Buffer: node hands back a plain
14814        // `Uint8Array` over the same bytes.
14815        Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14816        Some(_) => CloneProto::Same,
14817        // A boxed primitive keeps its wrapper; anything else — a user class
14818        // instance included — becomes a plain object.
14819        None if wrapped_primitive(v).is_some() => CloneProto::Same,
14820        None => CloneProto::Plain,
14821    }
14822}
14823
14824/// Which prototype a clone gets: the source's, a named builtin's, or none.
14825enum CloneProto {
14826    Same,
14827    Ctor(String),
14828    Plain,
14829}
14830
14831// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14832
14833/// A short `Name: message` string for an error value (used when an await
14834/// rejection unwinds as a thrown error).
14835pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14836    if let Some(JsObj::Object(props)) = h.get(v) {
14837        let name = props
14838            .get("name")
14839            .map(|x| h.str_of(x))
14840            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14841            .unwrap_or_else(|| "Error".into());
14842        if let Some(m) = props.get("message") {
14843            return format!("{name}: {}", h.str_of(m));
14844        }
14845        return name;
14846    }
14847    h.str_of(v)
14848}
14849
14850/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14851/// value)`, or `() => { throw reason }` on the reject path.
14852///
14853/// Returning the carried value directly — what this used to do — skipped both
14854/// halves. A promise returned by the callback was never awaited, so the
14855/// ordinary async-cleanup shape
14856///
14857/// ```text
14858/// work().finally(() => closeConnection()).then(next)
14859/// ```
14860///
14861/// ran `next` before the connection had closed. And the chain settled three
14862/// microtask ticks early, which is observable in ordering against any other
14863/// chain, not just against a timer.
14864///
14865/// A rejection from the callback's own promise wins over the carried value, so
14866/// no reject handler is attached here: it propagates on its own.
14867fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
14868    // PromiseResolve (27.2.4.7) returns an argument that is already a promise
14869    // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
14870    // thenable takes to adopt it, which showed up as a callback returning a
14871    // rejected promise settling one tick late against every other chain.
14872    let p = match with_host(|h| h.promise_id(&result)) {
14873        Some(_) => result,
14874        None => {
14875            let fresh = with_host(|h| h.new_promise());
14876            if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
14877                host::resolve_promise_val(pid, result);
14878            }
14879            fresh
14880        }
14881    };
14882    let cell = with_host(|h| h.new_array(vec![carried]));
14883    let idx = match cell {
14884        Value::Obj(i) => i,
14885        _ => 0,
14886    };
14887    let tag = if rethrow { "finrethrow" } else { "finret" };
14888    let thunk = make_builtin(format!("@@{tag}:{idx}"));
14889    host::promise_then(&p, thunk, Value::Undef)
14890}
14891
14892fn make_builtin(name: String) -> Value {
14893    with_host(|h| h.alloc(JsObj::Builtin(name)))
14894}
14895
14896/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
14897/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
14898///
14899/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
14900/// an EXPLICIT link, so an object on the default prototype reported `null`:
14901/// `({}).__proto__ === Object.prototype` was false while
14902/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
14903/// the three cannot drift apart again.
14904pub fn prototype_of(v: &Value) -> Value {
14905    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
14906    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
14907    // not `Function.prototype`. This is the class-side half of the subclass
14908    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
14909    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
14910        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
14911    }
14912    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
14913    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
14914    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
14915    // `ClassVal.parent`, but the link itself was invisible, so
14916    // `Object.getPrototypeOf(B) === A` read false and any library walking the
14917    // constructor chain — rather than calling a static — saw a base class.
14918    // A base class keeps the default answer below (`Function.prototype`).
14919    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
14920        if let Some(parent) = c.parent {
14921            return parent;
14922        }
14923    }
14924    // `Object.create(null)` and friends really do have a null prototype.
14925    if with_host(|h| h.has_null_proto(v)) {
14926        return with_host(|h| h.null());
14927    }
14928    // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
14929    // reported itself, because the fallback below answers by constructor name
14930    // and a plain object's is `Object` — an infinite chain to anything walking
14931    // it.
14932    if with_host(|h| h.strict_eq(v, &h.object_proto())) {
14933        return with_host(|h| h.null());
14934    }
14935    // Every OTHER builtin prototype namespace (`Array.prototype`,
14936    // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
14937    // would send it back to a namespace handle for its own constructor.
14938    if matches!(
14939        with_host(|h| h.get(v).cloned()),
14940        Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
14941    ) {
14942        return with_host(|h| h.object_proto());
14943    }
14944    if let Some(p) = with_host(|h| h.proto_of(v)) {
14945        return p;
14946    }
14947    // A builtin exotic with no explicit `[[Prototype]]` link reports its
14948    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
14949    // Array.prototype`), which `strict_eq` compares by name. A plain object
14950    // reports the one real `Object.prototype` object.
14951    with_host(|h| {
14952        h.ensure_native_protos();
14953        match default_ctor_name(h, v) {
14954            Some("Object") => h.object_proto(),
14955            // `String`/`Number`/`Boolean` own REAL prototype objects, so a
14956            // primitive must report that object and not a fresh namespace
14957            // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
14958            // compares a thunk against the real object and reads false.
14959            Some(c) => h
14960                .native_proto(c)
14961                .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
14962            None => h.null(),
14963        }
14964    })
14965}
14966
14967/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
14968/// internal resolve/reject functions.
14969/// A fresh promise built through the SPECIES constructor, when a `Promise`
14970/// static was reached through a subclass.
14971///
14972/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
14973/// combinator builds its result with `this` (27.2.4.x). They all allocated a
14974/// plain promise, so nothing a subclass produced was an instance of it. The
14975/// executor is a no-op: the result is settled through its promise id, which is
14976/// what the ordinary path does too.
14977fn promise_species_create() -> Result<Option<Value>, String> {
14978    let Some(ctor) = host::current_static_this() else {
14979        return Ok(None);
14980    };
14981    if !matches!(
14982        with_host(|h| h.kind_of(&ctor)),
14983        Some(ObjKind::Class) | Some(ObjKind::Func)
14984    ) {
14985        return Ok(None);
14986    }
14987    let species = match get_property(&ctor, "@@species") {
14988        Ok(Value::Undef) => ctor,
14989        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
14990        Ok(s) => s,
14991        Err(_) => ctor,
14992    };
14993    if !matches!(
14994        with_host(|h| h.kind_of(&species)),
14995        Some(ObjKind::Class) | Some(ObjKind::Func)
14996    ) {
14997        return Ok(None);
14998    }
14999    let noop = make_builtin("@@pnoop".to_string());
15000    let p = host::construct(&species, vec![noop])?;
15001    // Only usable if the subclass really produced a promise; a constructor that
15002    // returned something else has no id to settle.
15003    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15004}
15005
15006/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15007/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15008///
15009/// Distinct from `promise_species_create`, which answers for a STATIC reached
15010/// through a subclass. Here the subclass comes from the receiver itself, so
15011/// `P.resolve(1).then(f)` is also a `P`.
15012pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15013    // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15014    // which has no `constructor` entry of its own.
15015    let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15016    if !matches!(
15017        with_host(|h| h.kind_of(&ctor)),
15018        Some(ObjKind::Class) | Some(ObjKind::Func)
15019    ) {
15020        return Ok(None);
15021    }
15022    let species = match get_property(&ctor, "@@species") {
15023        Ok(Value::Undef) => ctor,
15024        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15025        Ok(s) => s,
15026        Err(_) => ctor,
15027    };
15028    if !matches!(
15029        with_host(|h| h.kind_of(&species)),
15030        Some(ObjKind::Class) | Some(ObjKind::Func)
15031    ) {
15032        return Ok(None);
15033    }
15034    let noop = make_builtin("@@pnoop".to_string());
15035    let p = host::construct(&species, vec![noop])?;
15036    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15037}
15038
15039fn new_promise(executor: Value) -> Result<Value, String> {
15040    let p = with_host(|h| h.new_promise());
15041    let id = with_host(|h| h.promise_id(&p).unwrap());
15042    let res = make_builtin(format!("@@presolve:{id}"));
15043    let rej = make_builtin(format!("@@preject:{id}"));
15044    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15045        // A throw in the executor rejects the promise.
15046        let ev = host::take_exc_or_error(&e);
15047        host::reject_promise_val(id, ev);
15048    }
15049    Ok(p)
15050}
15051
15052/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15053/// already-settled promise.
15054pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15055    promise_resolve(v)
15056}
15057
15058fn promise_resolve(v: Value) -> Result<Value, String> {
15059    if let Some(p) = promise_species_create()? {
15060        let id = with_host(|h| h.promise_id(&p).unwrap());
15061        host::resolve_promise_val(id, v);
15062        return Ok(p);
15063    }
15064    Ok(host::promise_of(&v))
15065}
15066fn promise_reject(v: Value) -> Result<Value, String> {
15067    let p = match promise_species_create()? {
15068        Some(p) => p,
15069        None => with_host(|h| h.new_promise()),
15070    };
15071    let id = with_host(|h| h.promise_id(&p).unwrap());
15072    host::reject_promise_val(id, v);
15073    Ok(p)
15074}
15075
15076/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15077/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15078/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15079/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15080/// callers that hand the resolver to an event listener.
15081pub fn pending_promise_with_resolver() -> (Value, Value) {
15082    let p = with_host(|h| h.new_promise());
15083    let id = with_host(|h| h.promise_id(&p).unwrap());
15084    let resolve = make_builtin(format!("@@presolve:{id}"));
15085    (p, resolve)
15086}
15087
15088/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15089///
15090/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15091/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15092/// without the two running together, and it escapes the punctuation that is
15093/// meaningful inside a character class or a group name.
15094fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15095    let v = arg0(&args);
15096    if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15097        return Err(host::type_error("input argument must be a string"));
15098    }
15099    let s = with_host(|h| h.str_of(&v));
15100    // Punctuation that is escaped by CODE POINT rather than with a backslash.
15101    // Measured against node over the whole ASCII range, not taken from a list:
15102    // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15103    const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15104    const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15105    let mut out = String::with_capacity(s.len());
15106    for (i, c) in s.chars().enumerate() {
15107        // A leading ASCII alphanumeric, and only a leading one.
15108        if i == 0 && c.is_ascii_alphanumeric() {
15109            out.push_str(&format!("\\x{:02x}", c as u32));
15110            continue;
15111        }
15112        if SYNTAX.contains(c) {
15113            out.push('\\');
15114            out.push(c);
15115            continue;
15116        }
15117        match c {
15118            '\t' => out.push_str("\\t"),
15119            '\n' => out.push_str("\\n"),
15120            '\u{b}' => out.push_str("\\v"),
15121            '\u{c}' => out.push_str("\\f"),
15122            '\r' => out.push_str("\\r"),
15123            _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15124                let n = c as u32;
15125                if n <= 0xff {
15126                    out.push_str(&format!("\\x{n:02x}"));
15127                } else {
15128                    out.push_str(&format!("\\u{n:04x}"));
15129                }
15130            }
15131            _ => out.push(c),
15132        }
15133    }
15134    Ok(with_host(|h| h.new_str(out)))
15135}
15136
15137/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15138/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15139/// Unicode but not to ECMAScript, and node leaves both alone.
15140fn is_regex_escape_space(c: char) -> bool {
15141    matches!(
15142        c,
15143        '\u{a0}' | '\u{1680}' | '\u{2000}'
15144            ..='\u{200a}'
15145                | '\u{2028}'
15146                | '\u{2029}'
15147                | '\u{202f}'
15148                | '\u{205f}'
15149                | '\u{3000}'
15150                | '\u{feff}'
15151    )
15152}
15153
15154/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15155/// object that merely INHERITS from `Error.prototype` is not one.
15156fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15157    let v = arg0(&args);
15158    Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15159}
15160
15161/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15162/// and `Object.prototype.toString`'s step 9 both test.
15163///
15164/// The brand is the OWN `stack` an error is built with (a `DOMException`
15165/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15166/// neither, which is why inheriting from an error prototype does not make a
15167/// value an error. Shared so the two cannot disagree — branding by a chain
15168/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15169/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15170/// on the same value already said false.
15171pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15172    match h.get(v) {
15173        Some(JsObj::Object(p)) => {
15174            p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15175        }
15176        _ => false,
15177    }
15178}
15179
15180/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15181/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15182/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15183/// costs a tick that this does not.
15184fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15185    let f = arg0(&args);
15186    // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15187    // returns a rejected promise, so the surrounding `try` never sees it.
15188    if !with_host(|h| host::is_callable(h, &f)) {
15189        // Node names the TYPE alongside the value — `number 5 is not a
15190        // function` — which the ordinary call-site message does not. A plain
15191        // object and a symbol name only the type; `null` names both.
15192        let shown = with_host(|h| {
15193            let kind = h.type_of(&f);
15194            match kind {
15195                "undefined" => "undefined".to_string(),
15196                "symbol" | "bigint" => kind.to_string(),
15197                "object" if h.is_null(&f) => "object null".to_string(),
15198                "object" => "object".to_string(),
15199                "string" => format!("string \"{}\"", h.str_of(&f)),
15200                _ => format!("{kind} {}", h.str_of(&f)),
15201            }
15202        });
15203        let p = with_host(|h| h.new_promise());
15204        let id = with_host(|h| h.promise_id(&p).unwrap());
15205        let reject = make_builtin(format!("@@preject:{id}"));
15206        let err =
15207            with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15208        host::invoke(&reject, vec![err], None)?;
15209        return Ok(p);
15210    }
15211    let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15212    let p = with_host(|h| h.new_promise());
15213    let id = with_host(|h| h.promise_id(&p).unwrap());
15214    let resolve = make_builtin(format!("@@presolve:{id}"));
15215    let reject = make_builtin(format!("@@preject:{id}"));
15216    let promise = p;
15217    match host::invoke(&f, rest, None) {
15218        Ok(v) => {
15219            host::invoke(&resolve, vec![v], None)?;
15220        }
15221        Err(e) => {
15222            // The thrown VALUE, not a re-synthesis of its rendering: a callback
15223            // that throws a `TypeError` must reject with that object, and
15224            // rebuilding it from the message string flattened it to a plain
15225            // `Error` whose message was the rendered `Uncaught TypeError: t`.
15226            let err =
15227                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15228            with_host(|h| {
15229                h.error = None;
15230                h.exc = None;
15231            });
15232            host::invoke(&reject, vec![err], None)?;
15233        }
15234    }
15235    Ok(promise)
15236}
15237
15238fn promise_with_resolvers() -> Result<Value, String> {
15239    let p = with_host(|h| h.new_promise());
15240    let id = with_host(|h| h.promise_id(&p).unwrap());
15241    let resolve = make_builtin(format!("@@presolve:{id}"));
15242    let reject = make_builtin(format!("@@preject:{id}"));
15243    let mut props: IndexMap<String, Value> = IndexMap::new();
15244    props.insert("promise".into(), p);
15245    props.insert("resolve".into(), resolve);
15246    props.insert("reject".into(), reject);
15247    Ok(with_host(|h| h.new_object(props)))
15248}
15249
15250/// A promise already rejected with `e` — what every combinator hands back when
15251/// the ITERABLE misbehaves.
15252///
15253/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15254/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15255/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15256/// and the caller saw the error at the call site instead.
15257fn rejected_promise(e: String) -> Value {
15258    let p = with_host(|h| h.new_promise());
15259    let id = with_host(|h| h.promise_id(&p).unwrap());
15260    let reject = make_builtin(format!("@@preject:{id}"));
15261    let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15262    with_host(|h| {
15263        h.error = None;
15264        h.exc = None;
15265    });
15266    let _ = host::invoke(&reject, vec![err], None);
15267    p
15268}
15269
15270#[derive(Clone, Copy)]
15271enum AllMode {
15272    All,
15273    AllSettled,
15274}
15275
15276/// `Promise.all` / `Promise.allSettled`.
15277fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15278    let items = match host::iter_all(&arg0(&args)) {
15279        Ok(v) => v,
15280        Err(e) => return Ok(rejected_promise(e)),
15281    };
15282    // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15283    // subclass the promise it hands back is an instance of that subclass.
15284    let result = match promise_species_create()? {
15285        Some(p) => p,
15286        None => with_host(|h| h.new_promise()),
15287    };
15288    let rid = with_host(|h| h.promise_id(&result).unwrap());
15289    let n = items.len();
15290    if n == 0 {
15291        let empty = with_host(|h| h.new_array(Vec::new()));
15292        host::resolve_promise_val(rid, empty);
15293        return Ok(result);
15294    }
15295    // Shared mutable accumulator via Rc<RefCell<…>>.
15296    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15297    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15298    for (i, it) in items.into_iter().enumerate() {
15299        let ap = host::promise_of(&it);
15300        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15301        let slots = slots.clone();
15302        let remaining = remaining.clone();
15303        host::subscribe_native(
15304            aid,
15305            Box::new(move |state, val| {
15306                let settled = match mode {
15307                    AllMode::All => {
15308                        if state == host::PromiseState::Rejected {
15309                            host::reject_promise_val(rid, val);
15310                            return Ok(());
15311                        }
15312                        val
15313                    }
15314                    AllMode::AllSettled => with_host(|h| {
15315                        let mut m: IndexMap<String, Value> = IndexMap::new();
15316                        if state == host::PromiseState::Rejected {
15317                            m.insert("status".into(), h.new_str("rejected"));
15318                            m.insert("reason".into(), val);
15319                        } else {
15320                            m.insert("status".into(), h.new_str("fulfilled"));
15321                            m.insert("value".into(), val);
15322                        }
15323                        h.new_object(m)
15324                    }),
15325                };
15326                slots.borrow_mut()[i] = settled;
15327                let mut r = remaining.borrow_mut();
15328                *r -= 1;
15329                if *r == 0 {
15330                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15331                    host::resolve_promise_val(rid, arr);
15332                }
15333                Ok(())
15334            }),
15335        );
15336    }
15337    Ok(result)
15338}
15339
15340/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15341fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15342    let items = match host::iter_all(&arg0(&args)) {
15343        Ok(v) => v,
15344        Err(e) => return Ok(rejected_promise(e)),
15345    };
15346    // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15347    let result = match promise_species_create()? {
15348        Some(p) => p,
15349        None => with_host(|h| h.new_promise()),
15350    };
15351    let rid = with_host(|h| h.promise_id(&result).unwrap());
15352    let n = items.len();
15353    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15354    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15355    for (i, it) in items.into_iter().enumerate() {
15356        let ap = host::promise_of(&it);
15357        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15358        let errors = errors.clone();
15359        let remaining = remaining.clone();
15360        host::subscribe_native(
15361            aid,
15362            Box::new(move |state, val| {
15363                if any {
15364                    if state == host::PromiseState::Fulfilled {
15365                        host::resolve_promise_val(rid, val);
15366                    } else {
15367                        errors.borrow_mut()[i] = val;
15368                        let mut r = remaining.borrow_mut();
15369                        *r -= 1;
15370                        if *r == 0 {
15371                            // All rejected → AggregateError carrying every reason.
15372                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15373                            let msg = with_host(|h| h.new_str("All promises were rejected"));
15374                            let agg = make_error_inner("AggregateError", &[reasons, msg]);
15375                            host::reject_promise_val(rid, agg);
15376                        }
15377                    }
15378                } else if state == host::PromiseState::Rejected {
15379                    host::reject_promise_val(rid, val);
15380                } else {
15381                    host::resolve_promise_val(rid, val);
15382                }
15383                Ok(())
15384            }),
15385        );
15386    }
15387    Ok(result)
15388}
15389
15390/// `.then` / `.catch` / `.finally` on a promise.
15391fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15392    match name {
15393        "then" => Ok(host::promise_then(
15394            recv,
15395            args.first().cloned().unwrap_or(Value::Undef),
15396            args.get(1).cloned().unwrap_or(Value::Undef),
15397        )),
15398        "catch" => Ok(host::promise_then(
15399            recv,
15400            Value::Undef,
15401            args.first().cloned().unwrap_or(Value::Undef),
15402        )),
15403        "finally" => {
15404            let cb = arg0(&args);
15405            // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15406            // as BOTH handlers, and `then` ignores a non-callable one — so the
15407            // value or reason simply passes through. Building the thunks
15408            // regardless meant `p.finally(null)` tried to call `null`.
15409            if !with_host(|h| host::is_callable(h, &cb)) {
15410                return Ok(host::promise_then(recv, cb.clone(), cb));
15411            }
15412            let i = match cb {
15413                Value::Obj(i) => i,
15414                _ => 0,
15415            };
15416            let pass = make_builtin(format!("@@finpass:{i}"));
15417            let throw = make_builtin(format!("@@finthrow:{i}"));
15418            Ok(host::promise_then(recv, pass, throw))
15419        }
15420        _ => Err(host::type_error(&format!(
15421            "promise.{name} is not a function"
15422        ))),
15423    }
15424}
15425
15426fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15427    with_host(|h| {
15428        if next_tick {
15429            h.queue_nexttick(cb, args);
15430        } else {
15431            h.queue_micro(cb, args);
15432        }
15433    });
15434}
15435
15436/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15437/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15438/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15439///
15440/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15441/// fires, so it runs until cleared and — being referenced — holds the process
15442/// open exactly as in Node.
15443fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15444    let cb = arg0(&args);
15445    let delay = if name == "setImmediate" {
15446        -1.0 // before any 0ms timeout
15447    } else {
15448        args.get(1)
15449            .map(|d| with_host(|h| h.to_number(d)))
15450            .unwrap_or(0.0)
15451            .max(0.0)
15452    };
15453    let extra = if name == "setImmediate" {
15454        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15455    } else {
15456        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15457    };
15458    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15459    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15460    let interval = (name == "setInterval").then(|| delay.max(1.0));
15461    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15462    let tag = if name == "setImmediate" {
15463        "Immediate"
15464    } else {
15465        "Timeout"
15466    };
15467    crate::stdlib::timers::new_handle(id, tag)
15468}
15469
15470/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15471/// by the bare id it coerces to (code that stored `+timer` still works).
15472fn clear_timer(v: &Value) {
15473    let id =
15474        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15475    with_host(|h| h.cancel_timer(id));
15476}