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, argc: u8) -> Value {
327    // The fourth argument, when present, is the FuncDef carrying the class's
328    // source span.
329    let source_def = match argc {
330        4 => match vm.pop() {
331            Value::Int(n) => Some(n as usize),
332            _ => None,
333        },
334        _ => None,
335    };
336    let ctor = vm.pop();
337    let parent = vm.pop();
338    let name = sval(&vm.pop());
339    host::build_class(&name, parent, ctor, source_def)
340}
341
342fn b_def_member(vm: &mut VM, _: u8) -> Value {
343    let func = vm.pop();
344    let is_static = matches!(vm.pop(), Value::Bool(true));
345    let kind = match vm.pop() {
346        Value::Int(n) => n,
347        _ => 0,
348    };
349    let name = sval(&vm.pop());
350    let class_val = vm.pop();
351    host::define_member(&class_val, &name, kind, is_static, func);
352    class_val
353}
354
355fn b_def_field(vm: &mut VM, _: u8) -> Value {
356    // `name_anon`: the initializer was an anonymous function definition, so
357    // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
358    // decided by the compiler, not re-derived from the produced value.
359    let name_anon = matches!(vm.pop(), Value::Bool(true));
360    let thunk = vm.pop();
361    let name = sval(&vm.pop());
362    let class_val = vm.pop();
363    host::define_field(&class_val, &name, thunk, name_anon);
364    class_val
365}
366
367/// `super(...args)` in a derived constructor: run the parent constructor on the
368/// current `this`, then this class's field initializers.
369/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
370/// run time. Shares everything below with the fixed-arity form; only where the
371/// arguments come from differs.
372fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
373    let arr = vm.pop();
374    let args = host::iter_all(&arr).unwrap_or_default();
375    super_call_with(vm, args)
376}
377
378fn b_super_call(vm: &mut VM, argc: u8) -> Value {
379    let args = pop_n(vm, argc as usize);
380    super_call_with(vm, args)
381}
382
383fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
384    let this = with_host(|h| h.current_this());
385    let this = match this {
386        Some(t) => t,
387        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
388    };
389    // The class whose constructor is running = the running method's home class.
390    let (parent, fields) = with_host(|h| h.super_context());
391    let (parent, fields) = match parent {
392        Some(p) => (p, fields),
393        None => return abort(vm, host::type_error("'super' keyword unexpected here")),
394    };
395    let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
396    let this = match host::super_construct(&parent, args, &this, &nt) {
397        Err(e) => return abort(vm, e),
398        // The parent returned an object of its own: 15.7.15 makes THAT the
399        // instance, so `this` is rebound to it for the rest of the constructor
400        // and it is what `new` hands back.
401        Ok(Some(replacement)) => {
402            with_host(|h| h.set_current_this(replacement.clone()));
403            replacement
404        }
405        Ok(None) => this,
406    };
407    if !with_host(|h| h.bind_super_this()) {
408        return abort(
409            vm,
410            "ReferenceError: Super constructor may only be called once".to_string(),
411        );
412    }
413    // Run this (derived) class's own instance-field initializers after super.
414    for (name, thunk, name_anon) in fields {
415        if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
416            return abort(vm, e);
417        }
418    }
419    Value::Undef
420}
421
422/// `super.name` — a method from the parent's prototype, or a getter's result.
423fn b_super_get(vm: &mut VM, _: u8) -> Value {
424    let name = sval(&vm.pop());
425    match with_host(|h| h.super_resolve(&name)) {
426        host::SuperRef::Data(v) => v,
427        host::SuperRef::Getter(getter) => {
428            let this = with_host(|h| h.current_this());
429            match host::invoke(&getter, Vec::new(), this) {
430                Ok(v) => v,
431                Err(e) => abort(vm, e),
432            }
433        }
434    }
435}
436
437/// Close every loop iterator parked on `vm`'s stack at the op now executing,
438/// innermost first. Called where a chunk is about to be halted abruptly, since
439/// the code that would ordinarily close them is being jumped over.
440///
441/// A close runs user code (a generator's `finally`), which can itself throw; the
442/// error is deliberately dropped, because it must not replace the completion
443/// that caused the unwind.
444fn close_parked_iters(vm: &mut VM) {
445    let n = host::parked_iters(vm);
446    if n == 0 {
447        return;
448    }
449    // The completion that caused the unwind is already pending on the host.
450    // Closing an iterator resumes ANOTHER generator, which settles its own
451    // signal/error state, so the pending one is saved across the close and put
452    // back — otherwise the outer `.return()` would be lost.
453    let saved = with_host(|h| (h.signal.take(), h.error.take()));
454    for _ in 0..n {
455        let it = vm.pop();
456        let _ = close_iterator(&it);
457    }
458    with_host(|h| {
459        h.signal = saved.0;
460        h.error = saved.1;
461    });
462}
463
464fn b_yield(vm: &mut VM, _: u8) -> Value {
465    let v = vm.pop();
466    match host::gen_yield(v) {
467        Ok(sent) => {
468            // A `.return()`/`.throw()` injected on resume sets a pending Return
469            // signal (or error); halt the chunk so the body unwinds through any
470            // enclosing `try/finally`, exactly like a source `return`/`throw`.
471            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
472                // Halting jumps past the loop exits, so the `for…of` / `yield*`
473                // iterators parked on this chunk's stack would be abandoned
474                // still-suspended. They sit directly beneath the yielded value
475                // (innermost last), and the compiler recorded how many are
476                // there for this exact op.
477                close_parked_iters(vm);
478                vm.ip = vm.chunk.ops.len();
479            }
480            sent
481        }
482        // An injected `.throw()` comes back as an error rather than a signal,
483        // and abandons the parked iterators the same way. The thrown value is
484        // already on the host as `exc`; `close_parked_iters` puts back whatever
485        // it saves, so the close cannot swallow it.
486        Err(e) => {
487            close_parked_iters(vm);
488            abort(vm, e)
489        }
490    }
491}
492
493/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
494///
495/// It called `JsHost::property_key` directly, which is the primitive-only half
496/// of the conversion, so an object key never ran `ToPrimitive`:
497/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
498/// member form `a[o] = 1` — which does go through `host::to_property_key` —
499/// keyed on `"TS"`. The two forms are the same abstract operation and now share
500/// the same implementation.
501fn b_propkey(vm: &mut VM, _: u8) -> Value {
502    let v = vm.pop();
503    match host::to_property_key(&v) {
504        Ok(k) => with_host(|h| h.new_str(k)),
505        Err(e) => abort(vm, e),
506    }
507}
508
509fn b_new_target(_vm: &mut VM, _: u8) -> Value {
510    with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
511}
512
513/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
514/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
515/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
516/// `0/0 === NaN`, so `/` is lowered here instead.
517///
518/// Being a builtin rather than a native op means it does NOT reach the numeric
519/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
520/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
521/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
522/// hook now, so `/` coerces exactly as `*` and `-` do.
523fn b_div(vm: &mut VM, _: u8) -> Value {
524    let b = vm.pop();
525    let a = vm.pop();
526    let r = numeric_hook(NumOp::Div, &a, &b);
527    finish(vm, r)
528}
529
530/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
531/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
532/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
533/// one code path that already handles it.
534fn b_pow(vm: &mut VM, _: u8) -> Value {
535    let b = vm.pop();
536    let a = vm.pop();
537    let r = numeric_hook(NumOp::Pow, &a, &b);
538    finish(vm, r)
539}
540
541/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
542fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
543    let excluded = vm.pop();
544    let obj = vm.pop();
545    // The excluded keys are normalized exactly as a property READ normalizes
546    // them, not merely stringified: a symbol key lives on the object under its
547    // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
548    // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
549    // symbol-keyed property in `rest`.
550    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
551        .unwrap_or_default()
552        .iter()
553        .filter_map(|v| host::to_property_key(v).ok())
554        .collect();
555    // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
556    // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
557    // uses, so an ACCESSOR is in the list — reading the property map directly
558    // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
559    // object with no `g` and never ran the getter.
560    // A PROXY answers from its traps — `ownKeys`, then a
561    // `getOwnPropertyDescriptor` per key to test enumerability — which
562    // `own_enum_key_names` cannot see. Rest over one produced an empty object
563    // and ran no traps at all.
564    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
565        let keys = match crate::proxy::own_keys(&obj) {
566            Ok(k) => k.unwrap_or_default(),
567            Err(e) => return abort(vm, e),
568        };
569        let mut pairs: Vec<(String, Value)> = Vec::new();
570        for k in keys {
571            if excl.contains(&k) {
572                continue;
573            }
574            // The enumerability test and the READ interleave per key, as node's
575            // trap log shows — testing every key first and then reading them
576            // all produced the right object through the wrong trap sequence.
577            match crate::proxy::own_enumerable(&obj, &k) {
578                Ok(false) => continue,
579                Ok(true) => {}
580                Err(e) => return abort(vm, e),
581            }
582            match get_property(&obj, &k) {
583                Ok(v) => pairs.push((k, v)),
584                Err(e) => return abort(vm, e),
585            }
586        }
587        return with_host(|h| h.new_object(pairs.into_iter().collect()));
588    }
589    let keys: Vec<String> = with_host(|h| {
590        // `own_enum_key_names` is the STRING half — the same list `Object.keys`
591        // gives, so an accessor is in it. The symbol-keyed half lives in the
592        // property map under the internal `@@sym:` spelling and has to be
593        // collected separately, since `Object.keys` deliberately omits it.
594        let mut ks = h.own_enum_key_names(&obj);
595        if let Some(JsObj::Object(m)) = h.get(&obj) {
596            for k in m.keys() {
597                if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
598                    ks.push(k.clone());
599                }
600            }
601        }
602        ks
603    })
604    .into_iter()
605    .filter(|k| {
606        // An internal slot (`@@native`, `@@bytes`, …) or a private class field
607        // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
608        // the two cases cannot be told apart by the prefix alone.
609        !excl.contains(k)
610            && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
611    })
612    .collect();
613    // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
614    // user code and re-entering the VM under the borrow aborts the process.
615    let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
616    for k in keys {
617        match get_property(&obj, &k) {
618            Ok(v) => pairs.push((k, v)),
619            Err(e) => return abort(vm, e),
620        }
621    }
622    with_host(|h| {
623        let props: IndexMap<String, Value> = pairs.into_iter().collect();
624        h.new_object(props)
625    })
626}
627
628// ── helpers ──────────────────────────────────────────────────────────────────
629
630fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
631    let mut v = Vec::with_capacity(n);
632    for _ in 0..n {
633        v.push(vm.pop());
634    }
635    v.reverse();
636    v
637}
638
639/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
640fn sval(v: &Value) -> String {
641    if let Value::Str(s) = v {
642        return (**s).clone();
643    }
644    with_host(|h| h.as_str(v)).unwrap_or_default()
645}
646
647/// The same string, without `sval`'s deep copy. Every identifier the compiler
648/// emits is a `Value::Str` constant, so a variable read or write that went
649/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
650/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
651/// handle is a refcount bump instead.
652fn sname(v: &Value) -> std::sync::Arc<String> {
653    match v {
654        Value::Str(s) => s.clone(),
655        _ => std::sync::Arc::new(sval(v)),
656    }
657}
658
659fn abort(vm: &mut VM, e: String) -> Value {
660    with_host(|h| h.error = Some(e));
661    vm.ip = vm.chunk.ops.len();
662    Value::Undef
663}
664
665/// Halt the chunk if a call left an error or non-local signal pending.
666fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
667    match r {
668        Ok(v) => {
669            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
670                vm.ip = vm.chunk.ops.len();
671            }
672            v
673        }
674        Err(e) => abort(vm, e),
675    }
676}
677
678// ── name handlers ─────────────────────────────────────────────────────────────
679
680/// The value a bare global identifier resolves to, or `None` if unbound.
681///
682/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
683/// which must agree: a name reachable one way and not the other is exactly the
684/// discrepancy that left `globalThis.process` undefined while `process` worked.
685pub(crate) fn global_binding(name: &str) -> Option<Value> {
686    global_binding_from(name, false)
687}
688
689/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
690///
691/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
692/// which is what makes `globalThis.Math` and `globalThis.process` work — but
693/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
694/// running its locals were readable off `globalThis`: `function f() { let zzq =
695/// 2; return typeof globalThis.zzq }` answered for a name the global object has
696/// never heard of. Only the globals map and the lazy builtins below may answer
697/// here.
698pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
699    global_binding_from(name, true)
700}
701
702fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
703    let bound = with_host(|h| {
704        if object_only {
705            h.read_global(name)
706        } else {
707            h.read_name(name)
708        }
709    });
710    if let Some(v) = bound {
711        return Some(v);
712    }
713    // Globals bound lazily: numeric sentinels + builtin namespaces.
714    match name {
715        "undefined" => return Some(Value::Undef),
716        "NaN" => return Some(Value::Float(f64::NAN)),
717        "Infinity" => return Some(Value::Float(f64::INFINITY)),
718        // One object, not a fresh one per read: `globalThis === globalThis` is
719        // `true` in JS, and `globalThis.x = 1` is readable back as
720        // `globalThis.x`. Both were false while each read minted a new object.
721        // `global` is Node's alias for the same object.
722        "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
723        // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
724        // node-flavoured module: `globalThis.crypto.randomUUID` exists while
725        // `globalThis.crypto.createHash` does not.
726        "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
727        _ => {}
728    }
729    if is_namespace(name) || is_known_builtin(name) {
730        return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
731    }
732    None
733}
734
735fn b_getlocal(vm: &mut VM, _: u8) -> Value {
736    let name = sname(&vm.pop());
737    // A module-top-level dead zone is tracked by NAME rather than by a parked
738    // marker, so that the marker is never reachable as `globalThis.<name>`. It
739    // only applies when nothing on the scope chain SHADOWS the name — a class's
740    // own inner binding for its name does exactly that while its static
741    // initializers run.
742    if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
743        return abort(vm, host::tdz_error(&name));
744    }
745    match global_binding(&name) {
746        // The binding EXISTS but has not reached its declaration yet.
747        Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
748        Some(v) => v,
749        None => abort(vm, host::ref_error(&name)),
750    }
751}
752
753/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
754/// top of the scope that declares it.
755fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
756    let name = sname(&vm.pop());
757    with_host(|h| h.hoist_tdz(&name));
758    Value::Undef
759}
760
761/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
762/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
763/// code — and, either way, never rebinds the name.
764const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
765
766fn readonly_global_error(name: &str) -> String {
767    host::type_error(&format!(
768        "Cannot assign to read only property '{name}' of object '#<Object>'"
769    ))
770}
771
772fn b_setlocal(vm: &mut VM, _: u8) -> Value {
773    let val = vm.pop();
774    let name = sname(&vm.pop());
775    // Sloppy assignment to a non-writable global is DISCARDED, not applied:
776    // `undefined = 1` used to rebind the name and make every later `undefined`
777    // read back as `1`.
778    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
779        return val;
780    }
781    // Assigning to a binding still in its temporal dead zone throws too —
782    // `{ x = 1; let x }` is a ReferenceError, not an initialization.
783    if with_host(|h| match h.read_name(&name) {
784        Some(v) => h.is_tdz(&v),
785        None => h.is_tdz_global(&name),
786    }) {
787        return abort(vm, host::tdz_error(&name));
788    }
789    // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
790    // immutable binding). This used to succeed silently.
791    if !with_host(|h| h.set_name(&name, val.clone())) {
792        return abort(vm, host::type_error("Assignment to constant variable."));
793    }
794    val
795}
796
797/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
798/// where sloppy code silently creates a global, strict code throws
799/// `ReferenceError: x is not defined`.
800///
801/// A separate opcode rather than a runtime flag: strictness is a static property
802/// of the code, so the compiler already knows which of the two an assignment is
803/// and sloppy code — everything in a CommonJS module without the directive —
804/// keeps the exact instruction it had.
805fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
806    let val = vm.pop();
807    let name = sname(&vm.pop());
808    if !binding_exists(&name) {
809        return abort(vm, host::ref_error(&name));
810    }
811    if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
812        return abort(vm, readonly_global_error(&name));
813    }
814    if !with_host(|h| h.set_name(&name, val.clone())) {
815        return abort(vm, host::type_error("Assignment to constant variable."));
816    }
817    val
818}
819
820/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
821/// materialised builtin namespace. `global_binding` answers the same question
822/// but ALLOCATES the namespace object to do it, which an assignment then throws
823/// away.
824fn binding_exists(name: &str) -> bool {
825    if with_host(|h| h.has_name(name)) {
826        return true;
827    }
828    matches!(
829        name,
830        "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
831    ) || is_namespace(name)
832        || is_known_builtin(name)
833}
834
835fn b_declare(vm: &mut VM, _: u8) -> Value {
836    let val = vm.pop();
837    let name = sname(&vm.pop());
838    with_host(|h| h.declare_name(&name, val.clone()));
839    val
840}
841
842/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
843/// assignment to the name throws instead of overwriting it.
844fn b_declare_const(vm: &mut VM, _: u8) -> Value {
845    let val = vm.pop();
846    let name = sname(&vm.pop());
847    with_host(|h| h.declare_const_name(&name, val.clone()));
848    val
849}
850
851/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
852/// open block scopes, so the name outlives the block it was written in.
853/// `var` hoisting: create the binding as `undefined` unless it already exists.
854fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
855    let name = sname(&vm.pop());
856    with_host(|h| h.hoist_var_name(&name));
857    Value::Undef
858}
859
860fn b_declare_var(vm: &mut VM, _: u8) -> Value {
861    let val = vm.pop();
862    let name = sname(&vm.pop());
863    with_host(|h| h.declare_var_name(&name, val.clone()));
864    val
865}
866
867fn b_push_scope(_: &mut VM, _: u8) -> Value {
868    with_host(|h| h.push_scope());
869    Value::Undef
870}
871
872fn b_pop_scope(_: &mut VM, _: u8) -> Value {
873    with_host(|h| h.pop_scope());
874    Value::Undef
875}
876
877fn b_copy_scope(_: &mut VM, _: u8) -> Value {
878    with_host(|h| h.copy_scope());
879    Value::Undef
880}
881
882fn b_delname(vm: &mut VM, _: u8) -> Value {
883    let name = sval(&vm.pop());
884    with_host(|h| h.del_name(&name));
885    Value::Bool(true)
886}
887
888fn b_this(vm: &mut VM, _: u8) -> Value {
889    if with_host(|h| h.this_state()) == host::ThisState::Pending {
890        return abort(vm, host::this_before_super_error());
891    }
892    with_host(|h| h.current_this().unwrap_or(Value::Undef))
893}
894
895fn b_load_null(_vm: &mut VM, _: u8) -> Value {
896    with_host(|h| h.null())
897}
898
899// ── attribute / item handlers ─────────────────────────────────────────────────
900
901fn b_getattr(vm: &mut VM, _: u8) -> Value {
902    let name = sval(&vm.pop());
903    let recv = vm.pop();
904    match get_property(&recv, &name) {
905        Ok(v) => v,
906        Err(e) => abort(vm, e),
907    }
908}
909
910/// Read `recv.name` (also the computed-key path for string keys). Walks own
911/// properties, accessors, and the prototype chain (class methods / getters).
912/// Read one small piece out of `recv`'s heap cell under a short borrow.
913///
914/// The closure must not call back into the host (`with_host` is a `RefCell`
915/// borrow and re-entering panics) — which is exactly why it hands back only the
916/// value needed: the caller re-enters freely afterwards. This replaces the old
917/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
918/// `String` just to look at it.
919fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
920    with_host(|h| h.get(recv).and_then(f))
921}
922
923/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
924/// reaches it without a closer link already owning `name`.
925///
926/// A proxy prototype answers only from the position it occupies in the chain: a
927/// nearer prototype that owns the key (as a data property or an accessor) still
928/// wins, exactly as `OrdinaryGet` walks one link at a time.
929pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
930    with_host(|h| {
931        let mut cur = h.proto_of(recv);
932        for _ in 0..100 {
933            let p = cur?;
934            match h.get(&p) {
935                Some(JsObj::Proxy { .. }) => return Some(p),
936                Some(JsObj::Object(props)) if props.contains_key(name) => return None,
937                _ => {}
938            }
939            if h.own_accessor(&p, name).is_some() {
940                return None;
941            }
942            cur = h.proto_of(&p);
943        }
944        None
945    })
946}
947
948/// The CommonJS wrapper's parameters. They are function locals in Node, not
949/// global-object properties, so `globalThis.require` is `undefined` and
950/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
951/// even though the bare `require` and `module` both work.
952const CJS_WRAPPER_LOCALS: &[&str] = &[
953    "require",
954    "module",
955    "exports",
956    "__filename",
957    "__dirname",
958    "__cjs_require",
959    "__cjs_resolve",
960];
961
962/// The globals node exposes as ENUMERABLE own properties of the global object —
963/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
964/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
965const ENUMERABLE_GLOBALS: &[&str] = &[
966    "global",
967    "clearImmediate",
968    "setImmediate",
969    "clearInterval",
970    "clearTimeout",
971    "setInterval",
972    "setTimeout",
973    "queueMicrotask",
974    "structuredClone",
975    "atob",
976    "btoa",
977    "performance",
978    "fetch",
979    "crypto",
980    "navigator",
981    "sessionStorage",
982];
983
984pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
985    // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
986    // when the receiver carries no such private element — it does NOT read back
987    // as `undefined`, which is what `C.prototype.method.call({})` used to do.
988    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
989        return Err(private_brand_message(name, false));
990    }
991    get_property_recv(recv, name, recv)
992}
993
994/// The `TypeError` a failed private brand check raises. Node words it two ways:
995/// a private METHOD or accessor names the class the receiver should have been an
996/// instance of, while a private FIELD names the member.
997pub fn private_brand_message(name: &str, writing: bool) -> String {
998    if with_host(|h| h.is_private_method(name)) {
999        if let Some(class) = with_host(|h| h.current_home_class_name()) {
1000            return host::type_error(&format!("Receiver must be an instance of class {class}"));
1001        }
1002    }
1003    let verb = if writing { "write" } else { "read" };
1004    let prep = if writing { "to" } else { "from" };
1005    host::type_error(&format!(
1006        "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
1007    ))
1008}
1009
1010/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1011/// from and is what a getter sees as `this`; it differs from `recv` only when the
1012/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1013/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1014/// passes `recv` itself.
1015/// Re-format an error's `.stack` header on its first read, the way V8 does.
1016///
1017/// The constructor could only stamp the name it was called with, so a subclass
1018/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1019/// before the first read — left a stale header. Node re-reads both properties at
1020/// format time, including one inherited from the prototype (`E.prototype.name`).
1021///
1022/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1023/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1024/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1025/// assignment is never clobbered by a later read.
1026/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1027/// the ordinary stack path can skip the hook round-trip when nothing custom is
1028/// installed.
1029pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1030
1031pub fn materialize_stack(recv: &Value) {
1032    let Some(frames) = with_host(|h| match h.get(recv) {
1033        Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1034        _ => None,
1035    }) else {
1036        return;
1037    };
1038    // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1039    // stack-introspection hook, which every source-map library installs). It was
1040    // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1041    // read bypassed it and handed back the default text.
1042    let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1043    if let Some(f) = prep.filter(|f| {
1044        // The default hook produces exactly what the fast path below produces,
1045        // so it is skipped rather than called.
1046        !matches!(
1047            with_host(|h| h.get(f).cloned()),
1048            Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1049        ) && matches!(
1050            with_host(|h| h.get(f).cloned()),
1051            Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1052        )
1053    }) {
1054        // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1055        // second materialization would re-enter this path forever.
1056        with_host(|h| {
1057            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1058                p.shift_remove("@@stackRaw");
1059            }
1060        });
1061        let limit = with_host(|h| h.stack_trace_limit());
1062        if let Ok(sites) = crate::module::callsite_stack(limit) {
1063            if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1064                with_host(|h| {
1065                    if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1066                        p.insert("stack".into(), out);
1067                    }
1068                });
1069                return;
1070            }
1071        }
1072    }
1073    with_host(|h| {
1074        let frames = h.str_of(&frames);
1075        let name = host::lookup_chain(h, recv, "name")
1076            .map(|v| h.str_of(&v))
1077            .unwrap_or_else(|| "Error".to_string());
1078        let message = host::lookup_chain(h, recv, "message")
1079            .map(|v| h.str_of(&v))
1080            .unwrap_or_default();
1081        let header = if message.is_empty() {
1082            name
1083        } else {
1084            format!("{name}: {message}")
1085        };
1086        let sv = h.new_str(format!("{header}{frames}"));
1087        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1088            p.insert("stack".into(), sv);
1089            p.shift_remove("@@stackRaw");
1090        }
1091    });
1092}
1093
1094pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1095    // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1096    // target. Checked before anything else so no ordinary-object shortcut can
1097    // read past the handler.
1098    if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1099        return Ok(v);
1100    }
1101    if with_host(|h| h.is_nullish(recv)) {
1102        return Err(host::type_error(&format!(
1103            "Cannot read properties of {} (reading '{name}')",
1104            with_host(|h| h.str_of(recv))
1105        )));
1106    }
1107    if name == "stack" {
1108        materialize_stack(recv);
1109    }
1110    // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1111    // internal slots, so they resolve here rather than out of a property map.
1112    if let Some(v) = dom_exception_slot(recv, name) {
1113        return Ok(v);
1114    }
1115    // A read off `globalThis` for a name the object does not own falls back to
1116    // the same lazy global binding the bare identifier gets. Without it the
1117    // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1118    // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1119    // `false` and any `globalThis.X` feature probe reported the feature missing.
1120    if with_host(|h| h.is_global_object(recv)) {
1121        let own = with_host(|h| match h.get(recv) {
1122            Some(JsObj::Object(p)) => p.contains_key(name),
1123            _ => false,
1124        });
1125        // The CommonJS wrapper's parameters are function locals in Node, not
1126        // global-object properties: `typeof globalThis.require` is `undefined`
1127        // there even though the bare `require` works.
1128        if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1129            if let Some(v) = global_object_binding(name) {
1130                return Ok(v);
1131            }
1132        }
1133    }
1134    // Accessor (own or inherited getter) takes precedence over the chain walk.
1135    // The getter runs with the RECEIVER as `this`, not the object that owns it.
1136    if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1137        return match getter {
1138            Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1139            None => Ok(Value::Undef), // set-only property reads as undefined
1140        };
1141    }
1142    // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1143    // one expose it to a plain read, not just to `Object.prototype.toString` —
1144    // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1145    // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1146    // Anything the receiver's own chain provides wins (a class may define its
1147    // own getter), so this is only the fallback.
1148    if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1149        if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1150            return Ok(with_host(|h| h.new_str(tag)));
1151        }
1152    }
1153    // `constructor`: a user class/function sets it on the prototype chain, and
1154    // that wins; otherwise every builtin instance reports its native
1155    // constructor (so `[].constructor`, `new Map().constructor`,
1156    // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1157    if name == "constructor" {
1158        if let Some(v) = with_host(|h| {
1159            match h.get(recv) {
1160                Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1161                _ => None,
1162            }
1163            .or_else(|| host::lookup_chain(h, recv, "constructor"))
1164        }) {
1165            return Ok(v);
1166        }
1167        // An intrinsic prototype the receiver's CHAIN reaches owns a
1168        // `constructor` too, and it wins over the receiver's own kind:
1169        // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1170        // Deciding from the kind alone also mis-named the receiver in every
1171        // message that renders one — the brand-check errors say `#<Map>`.
1172        if let Some(c) = chain_intrinsic_ctors(recv)
1173            .into_iter()
1174            .find(|c| is_builtin_ctor(c))
1175        {
1176            return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1177        }
1178        if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1179            return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1180        }
1181    }
1182    // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1183    // answers for EVERY object that inherits from it, not only plain ones —
1184    // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1185    // so an array, function or builtin instance read `undefined`. An object with
1186    // a null prototype inherits no such accessor and reads `undefined`, which is
1187    // why this is skipped there rather than answering `null`.
1188    if name == "__proto__"
1189        && !with_host(|h| h.has_null_proto(recv))
1190        && peek(recv, |o| match o {
1191            JsObj::Object(p) => Some(p.contains_key("__proto__")),
1192            _ => Some(false),
1193        }) != Some(true)
1194    {
1195        return Ok(prototype_of(recv));
1196    }
1197    // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1198    // method: it RUNS the getter with that prototype as `this`, and all but two
1199    // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1200    // answered `undefined`, so both the value and the failure were invisible.
1201    // Both representations of a prototype reach here — the namespace handles
1202    // and the real objects (`Symbol.prototype`, `String.prototype`).
1203    if let Some(ctor) = intrinsic_proto_of(recv) {
1204        if is_proto_accessor(&ctor, name) {
1205            return proto_getter_call(&ctor, name, recv);
1206        }
1207    }
1208    let kind = with_host(|h| h.kind_of(recv));
1209    #[allow(unused_mut)]
1210    let mut out = match kind {
1211        Some(ObjKind::Object) => {
1212            let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1213            // A view over a DETACHED buffer reports zero extent. Its own
1214            // `length`/`byteLength`/`byteOffset` properties still hold the old
1215            // numbers — the buffer does not know its views, so it cannot rewrite
1216            // them — and reading them straight back made a detached view still
1217            // look eight bytes long.
1218            if matches!(name, "length" | "byteLength" | "byteOffset")
1219                && crate::stdlib::typedarray::view_detached(recv)
1220            {
1221                match crate::stdlib::native_tag(recv).as_deref() {
1222                    Some("TypedArray") => return Ok(Value::Float(0.0)),
1223                    // A DataView THROWS where a typed array answers zero — its
1224                    // extent accessors are brand-checked and node reports the
1225                    // getter by name.
1226                    Some("DataView") => {
1227                        return Err(crate::stdlib::typedarray::detached_error(
1228                            "get DataView.prototype",
1229                            name,
1230                            false,
1231                        ))
1232                    }
1233                    _ => {}
1234                }
1235            }
1236            // Typed-array element read (`ta[i]`): elements live in a hidden
1237            // `@@elems`, not as own numeric props, so intercept integer keys.
1238            if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1239                if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1240                    return Ok(v);
1241                }
1242            }
1243            // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1244            // own numeric props, so integer keys read through to it.
1245            if numeric
1246                && peek(recv, |o| match o {
1247                    JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1248                    _ => None,
1249                })
1250                .unwrap_or(false)
1251            {
1252                return Ok(crate::stdlib::buffer::byte_get(recv, name));
1253            }
1254            if let Some(v) = peek(recv, |o| match o {
1255                JsObj::Object(p) => p.get(name).cloned(),
1256                _ => None,
1257            }) {
1258                v
1259            } else if let Some(link) = proxy_proto_link(recv, name) {
1260                // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1261                // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1262                // receiver, so the trap sees the child as `receiver` and `this`
1263                // inside a trap-served getter resolves to the child, not the
1264                // proxy. `lookup_chain` cannot do this: it reads property maps,
1265                // and a proxy has none.
1266                return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1267            } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1268                // A method / data property inherited from the prototype chain.
1269                v
1270            } else if crate::stdlib::native_tag(recv)
1271                .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1272                .unwrap_or(false)
1273            {
1274                // A native instance method read as a property (`server.listen`) →
1275                // a bound method, dispatched via `instance_call` when invoked.
1276                bound_method(recv, name)
1277            } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1278                // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1279                // read as `undefined` there — which is also what makes
1280                // `Object.create(null) + 1` the spec `TypeError` instead of a
1281                // silent `"[object Object]1"`.
1282                bound_method(recv, name)
1283            } else {
1284                Value::Undef
1285            }
1286        }
1287        Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1288            function_property(recv, name)
1289        }
1290        // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1291        // thunk here. It is a function value, so it answers the function
1292        // properties: `[].slice.name` was `undefined` where node reports
1293        // `slice`, and `String([].slice)` fell through to
1294        // `Object.prototype.toString`.
1295        Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1296        Some(ObjKind::Symbol) => match name {
1297            "description" => {
1298                match peek(recv, |o| match o {
1299                    JsObj::Symbol { desc, .. } => desc.clone(),
1300                    _ => None,
1301                }) {
1302                    Some(d) => with_host(|h| h.new_str(d)),
1303                    None => Value::Undef,
1304                }
1305            }
1306            "toString" => bound_method(recv, name),
1307            // Anything else a symbol answers, it inherits from
1308            // `Symbol.prototype`. The arm used to stop at `undefined`, so
1309            // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1310            // as absent even though the prototype defines both — a symbol is an
1311            // ordinary object for the purpose of a property LOOKUP, only its
1312            // methods are branded.
1313            _ => with_host(|h| {
1314                h.ensure_wrapper_protos();
1315                h.native_proto("Symbol")
1316            })
1317            .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1318            .unwrap_or(Value::Undef),
1319        },
1320        Some(ObjKind::BigInt) => {
1321            if matches!(
1322                name,
1323                "toString" | "valueOf" | "toLocaleString" | "constructor"
1324            ) {
1325                bound_method(recv, name)
1326            } else {
1327                Value::Undef
1328            }
1329        }
1330        Some(ObjKind::RegExp) => {
1331            // A RegExp holds no collection, so cloning the compiled pattern here
1332            // does not scale with any input size; `regexp_property` re-enters the
1333            // host to allocate `source`/`flags`, so it cannot run under a borrow.
1334            let r = peek(recv, |o| match o {
1335                JsObj::RegExp(r) => Some(r.clone()),
1336                _ => None,
1337            });
1338            match r {
1339                Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1340                    // An OWN property beats the prototype method of the same
1341                    // name, which is ordinary resolution order. It mattered once
1342                    // the symbol-keyed methods existed: `re[Symbol.match] =
1343                    // false` disowns the regexp label (7.2.8), and the method
1344                    // was shadowing the assignment so the value never took.
1345                    if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1346                        return v;
1347                    }
1348                    if crate::regexp::is_regexp_method(name) {
1349                        bound_method(recv, name)
1350                    } else {
1351                        Value::Undef
1352                    }
1353                }),
1354                None => Value::Undef,
1355            }
1356        }
1357        // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1358        // the read must be `undefined` rather than a live count.
1359        Some(ObjKind::Map) => {
1360            let (len, weak) = peek(recv, |o| match o {
1361                JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1362                _ => None,
1363            })
1364            .unwrap_or((0, false));
1365            match name {
1366                "size" if !weak => Value::Float(len as f64),
1367                "@@iterator" => bound_method(recv, name),
1368                _ if is_map_method(name) => bound_method(recv, name),
1369                _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1370            }
1371        }
1372        Some(ObjKind::Set) => {
1373            let (len, weak) = peek(recv, |o| match o {
1374                JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1375                _ => None,
1376            })
1377            .unwrap_or((0, false));
1378            match name {
1379                "size" if !weak => Value::Float(len as f64),
1380                "@@iterator" => bound_method(recv, name),
1381                _ if is_set_method(name) => bound_method(recv, name),
1382                _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1383            }
1384        }
1385        Some(ObjKind::Generator) => {
1386            // A generator IS its own iterator, so it answers for the matching
1387            // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1388            // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1389            // was `undefined` even though `for await` over it worked through a
1390            // different path.
1391            let want = if with_host(|h| h.is_async_gen_val(recv)) {
1392                "@@asyncIterator"
1393            } else {
1394                "@@iterator"
1395            };
1396            if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1397            {
1398                bound_method(recv, name)
1399            } else {
1400                with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1401            }
1402        }
1403        Some(ObjKind::Promise) => {
1404            if matches!(name, "then" | "catch" | "finally") {
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::Iter) => {
1411            if matches!(name, "next" | "return" | "@@iterator")
1412                || crate::stdlib::iterator::is_helper(name)
1413            {
1414                bound_method(recv, name)
1415            } else {
1416                with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1417            }
1418        }
1419        Some(ObjKind::Array) => {
1420            if name == "length" {
1421                let n = peek(recv, |o| match o {
1422                    JsObj::Array(items) => Some(items.len()),
1423                    _ => None,
1424                })
1425                .unwrap_or(0);
1426                Value::Float(n as f64)
1427            } else if let Ok(i) = name.parse::<usize>() {
1428                peek(recv, |o| match o {
1429                    JsObj::Array(items) => items.get(i).cloned(),
1430                    _ => None,
1431                })
1432                // An index PAST an `arguments` object's length is an ordinary
1433                // own property in the side table, since adding one must not
1434                // move `length`. The array read alone could not see it, so the
1435                // write was invisible to every later read.
1436                .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1437                .unwrap_or(Value::Undef)
1438            } else if name == "@@iterator"
1439                || is_object_method(name)
1440                // An `arguments` object is array-BACKED here but is not an
1441                // Array: node's exposes no `Array.prototype` method, which is
1442                // exactly why the idiom is `Array.prototype.slice.call(args)`.
1443                // Exposing them made `arguments.map` a function.
1444                || (is_array_method(name) && !is_arguments(recv))
1445            {
1446                bound_method(recv, name)
1447            } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1448                // Extra own props attached to an array (e.g. `RegExp.exec` result's
1449                // `.index`/`.input`/`.groups`).
1450                v
1451            } else {
1452                Value::Undef
1453            }
1454        }
1455        Some(ObjKind::Str) => {
1456            // `.length` and `s[i]` count UTF-16 code units, not code points.
1457            if name == "length" {
1458                let n = peek(recv, |o| match o {
1459                    JsObj::Str(s) => Some(crate::utf16::len(s)),
1460                    _ => None,
1461                })
1462                .unwrap_or(0);
1463                Value::Float(n as f64)
1464            } else if let Ok(i) = name.parse::<usize>() {
1465                match peek(recv, |o| match o {
1466                    JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1467                    _ => None,
1468                }) {
1469                    Some(c) => with_host(|h| h.new_str(c)),
1470                    None => Value::Undef,
1471                }
1472            } else if name == "@@iterator" || is_string_method(name) {
1473                bound_method(recv, name)
1474            } else {
1475                Value::Undef
1476            }
1477        }
1478        Some(ObjKind::Builtin) => {
1479            let ns = peek(recv, |o| match o {
1480                JsObj::Builtin(ns) => Some(ns.clone()),
1481                _ => None,
1482            })
1483            .unwrap_or_default();
1484            let v = namespace_property(&ns, name);
1485            // `Function.prototype`'s methods READ off a builtin function. The
1486            // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1487            // the read answered `undefined` — so `typeof Math.max.bind` was
1488            // `"undefined"`, and `String(Math.max)` found no `toString` to
1489            // invoke and fell back to `Object.prototype.toString`'s
1490            // `[object Function]` where node reports the native-code form.
1491            if matches!(v, Value::Undef)
1492                && is_function_method(name)
1493                && host::builtin_is_callable(&ns)
1494            {
1495                return Ok(bound_method(recv, name));
1496            }
1497            v
1498        }
1499        _ => {
1500            // Primitive numbers/booleans: method access -> bound method.
1501            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1502                bound_method(recv, name)
1503            } else {
1504                Value::Undef
1505            }
1506        }
1507    };
1508    // Every object INHERITS the `Object.prototype` methods, and each kind's
1509    // read arm above knows only its OWN. So `typeof new Map().toString`,
1510    // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1511    // answered `undefined` — for Map the CALL already worked, which is the
1512    // read and the dispatch disagreeing about the same method.
1513    //
1514    // Which prototype owns the name is decided by the same helper the `in`
1515    // operator uses, so the two cannot drift, and the result is the SHARED
1516    // intrinsic rather than a per-read thunk.
1517    // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1518    // accessor throws rather than answering, which is how a strict function
1519    // keeps its caller unreachable. It read back as `undefined` here, which a
1520    // feature probe reads as "not supported" rather than "forbidden".
1521    // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1522    // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1523    // and `arguments` are.
1524    if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1525        return Err(host::type_error(POISON_PILL));
1526    }
1527    if matches!(name, "caller" | "arguments")
1528        && matches!(
1529            with_host(|h| h.kind_of(recv)),
1530            Some(ObjKind::Func) | Some(ObjKind::Class)
1531        )
1532    {
1533        return poison_pill_read(recv);
1534    }
1535    // `arguments.callee` in SLOPPY code is the running function — the
1536    // pre-`class` self-reference idiom. It read back `undefined`.
1537    if name == "callee" && is_arguments(recv) {
1538        if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1539            return Ok(f);
1540        }
1541    }
1542    // A method SYNTHESIZED from the receiver's kind is only reachable while the
1543    // receiver's intrinsic prototype is still on its chain. `Object
1544    // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1545    // above answers from the kind alone and cannot know the link changed. Only
1546    // a synthesized value is dropped — the two shapes a method read produces —
1547    // and only when the receiver does not own the name itself.
1548    if matches!(
1549        with_host(|h| h.get(&out).cloned()),
1550        Some(JsObj::BoundMethod { .. })
1551    ) || matches!(
1552        with_host(|h| h.get(&out).cloned()),
1553        Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1554    ) {
1555        // The kind arms synthesize their OWN kind's methods, so that is the
1556        // prototype whose reachability decides. Clearing the value here lets
1557        // the `inherited_method_owner` fallback below re-supply the
1558        // `Object.prototype` form where one exists — which is why
1559        // `a.toString` stays a function after the link is replaced while
1560        // `a.join` does not.
1561        if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1562            out = Value::Undef;
1563        }
1564    }
1565    // A key the receiver does not OWN is looked up on its prototype chain. The
1566    // exotic arms above answer from their own storage and stop, so an array
1567    // given a prototype inherited nothing through a read: with
1568    // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1569    // index and at one past the end, while `1 in a` already answered true —
1570    // the two views of the same question disagreeing. An accessor was found
1571    // (`lookup_accessor` walks), so only DATA properties went missing.
1572    //
1573    // A plain object's arm already consults the chain, and an array with no
1574    // explicit prototype has no links to walk, so this changes neither.
1575    if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1576        if matches!(out, Value::Undef) {
1577            if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1578                return Ok(v);
1579            }
1580        }
1581        // Then a monkey-patched intrinsic prototype member, which shadows the
1582        // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1583        // BE `f`. An explicitly-set prototype above wins over it, as the chain
1584        // order requires.
1585        if let Some(v) = inherited_builtin_static(recv, name) {
1586            return Ok(v);
1587        }
1588    }
1589    if matches!(out, Value::Undef) && !name.starts_with('#') {
1590        if let Some(owner) = inherited_method_owner(recv, name) {
1591            // An INHERITED accessor runs, it does not hand back a thunk, and
1592            // its brand check is about the receiver's internal slot rather than
1593            // its chain — `Object.create(Map.prototype).size` throws in node
1594            // even though `Map.prototype` is right there above it. This
1595            // answered `undefined`, which is the value a real Map would never
1596            // give and a plain object should never reach.
1597            if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1598                return proto_getter_call(owner, name, recv);
1599            }
1600            let key = format!("@proto:{owner}:{name}");
1601            if builtin_meta(&key).is_some() {
1602                return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1603            }
1604            // A DATA member of the prototype — `Array.prototype[Symbol
1605            // .unscopables]` is an object, not a method, so it is in neither
1606            // function table. Read it off the prototype itself rather than
1607            // answering `undefined`: an instance inherits it.
1608            let v = namespace_property(&format!("{owner}.prototype"), name);
1609            if !matches!(v, Value::Undef) {
1610                return Ok(v);
1611            }
1612        }
1613    }
1614    Ok(out)
1615}
1616
1617/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1618/// object literal because the module cache is the single source of truth: a
1619/// populated copy would answer reads correctly and silently ignore a `delete`,
1620/// which is the operation the property exists for.
1621pub const REQUIRE_CACHE: &str = "__cjs_cache";
1622
1623/// The builtin constructor name for a value with no own/inherited `constructor`
1624/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1625/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1626/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1627/// whose `.constructor.name` is `""` in Node — not worth modelling).
1628fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1629    match h.get(recv) {
1630        Some(JsObj::Array(_)) => Some("Array"),
1631        Some(JsObj::Object(props)) => {
1632            // A native instance reports its own constructor, not Object — e.g.
1633            // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1634            // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1635            // the `@@native` tag off the already-borrowed host (calling
1636            // `native_tag`, which re-enters `with_host`, would double-borrow).
1637            match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1638                Some("Buffer") => Some("Buffer"),
1639                Some("URL") => Some("URL"),
1640                Some("Date") => Some("Date"),
1641                Some("WeakRef") => Some("WeakRef"),
1642                Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1643                Some("TextEncoder") => Some("TextEncoder"),
1644                Some("TextDecoder") => Some("TextDecoder"),
1645                Some("EventEmitter") => Some("EventEmitter"),
1646                Some("Timeout") => Some("Timeout"),
1647                Some("Immediate") => Some("Immediate"),
1648                _ => Some("Object"),
1649            }
1650        }
1651        Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1652        Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1653        Some(JsObj::Promise { .. }) => Some("Promise"),
1654        Some(JsObj::Str(_)) => Some("String"),
1655        Some(JsObj::Symbol { .. }) => Some("Symbol"),
1656        Some(JsObj::BigInt(_)) => Some("BigInt"),
1657        Some(JsObj::RegExp(_)) => Some("RegExp"),
1658        Some(JsObj::Iter { .. }) => Some("Iterator"),
1659        Some(JsObj::Func(f)) => {
1660            // A generator or async function is NOT an ordinary function: its
1661            // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1662            // variants'), and so is its `constructor`. All three reported plain
1663            // `Function`, so `g.constructor.name` was `Function` where node
1664            // says `GeneratorFunction`.
1665            Some(match h.funcs.get(f.def_id) {
1666                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1667                Some(d) if d.is_generator => "GeneratorFunction",
1668                Some(d) if d.is_async => "AsyncFunction",
1669                _ => "Function",
1670            })
1671        }
1672        Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1673        _ => match recv {
1674            Value::Float(_) | Value::Int(_) => Some("Number"),
1675            Value::Bool(_) => Some("Boolean"),
1676            _ => None,
1677        },
1678    }
1679}
1680
1681/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1682/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1683/// `process`), whose `.name` is `undefined` in Node.
1684///
1685/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1686/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1687/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1688/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1689/// so a subclass is its own species unless it overrides the getter.
1690fn has_species(name: &str) -> bool {
1691    matches!(
1692        name,
1693        "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1694    ) || crate::stdlib::typedarray::is_ctor(name)
1695}
1696
1697fn is_builtin_ctor(name: &str) -> bool {
1698    matches!(
1699        name,
1700        "Array"
1701            | "Object"
1702            | "Number"
1703            | "String"
1704            | "Boolean"
1705            | "Symbol"
1706            | "Function"
1707            | "Map"
1708            | "Set"
1709            | "WeakMap"
1710            | "WeakSet"
1711            | "Promise"
1712            | "BigInt"
1713            | "Iterator"
1714            | "RegExp"
1715            | "Date"
1716            | "ArrayBuffer"
1717            | "DataView"
1718            | "Uint8Array"
1719            | "Int8Array"
1720            | "Uint8ClampedArray"
1721            | "Int16Array"
1722            | "Uint16Array"
1723            | "Int32Array"
1724            | "Uint32Array"
1725            | "Float32Array"
1726            | "Float64Array"
1727            | "BigInt64Array"
1728            | "BigUint64Array"
1729            | "WeakRef"
1730            | "FinalizationRegistry"
1731            | "TextEncoder"
1732            | "TextDecoder"
1733            | "IncomingMessage"
1734            | "ServerResponse"
1735            | "EventEmitter"
1736            | "Buffer"
1737            | "URL"
1738            | "URLSearchParams"
1739            | "Timeout"
1740            | "Immediate"
1741    ) || host::ERROR_NAMES.contains(&name)
1742        // The stream base classes are constructors too, and `require('stream')`
1743        // IS `Stream`, so `require('stream').name` has to answer.
1744        || crate::stdlib::stream::is_class(name)
1745}
1746
1747/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1748/// thunk can look its `name`/`length` up in the same table a
1749/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1750/// builtin constructor to name (a native stdlib instance, whose methods are
1751/// node's own JS and have no specified arity).
1752fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1753    let ctor = with_host(|h| default_ctor_name(h, recv))?;
1754    Some(format!("@proto:{ctor}:{method}"))
1755}
1756
1757/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1758/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1759/// table knows the method, because inventing an arity is worse than the
1760/// `undefined` a caller can test for.
1761fn bound_method_property(recv: &Value, name: &str) -> Value {
1762    let method = peek(recv, |o| match o {
1763        JsObj::BoundMethod { name, .. } => Some(name.clone()),
1764        _ => None,
1765    })
1766    .unwrap_or_default();
1767    let key = peek(recv, |o| match o {
1768        JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1769        _ => None,
1770    })
1771    .and_then(|inner| bound_method_key(&inner, &method));
1772    let meta = key.as_deref().and_then(builtin_meta);
1773    match name {
1774        "name" => {
1775            let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1776            with_host(|h| h.new_str(n))
1777        }
1778        "length" => match meta {
1779            Some((_, len)) => Value::Float(len as f64),
1780            None => Value::Undef,
1781        },
1782        _ if is_function_method(name) => bound_method(recv, name),
1783        _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1784    }
1785}
1786
1787fn bound_method(recv: &Value, name: &str) -> Value {
1788    // An ECMAScript intrinsic is ONE function object shared by every instance:
1789    // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1790    // true. Reading one off an instance used to mint a fresh thunk bound to that
1791    // instance, so every such comparison answered false — and a detached method
1792    // kept working on the receiver it was read off, where node throws because it
1793    // has no `this` at all.
1794    if let Some(key) = bound_method_key(recv, name) {
1795        if builtin_meta(&key).is_some() {
1796            return with_host(|h| h.alloc(JsObj::Builtin(key)));
1797        }
1798    }
1799    with_host(|h| {
1800        h.alloc(JsObj::BoundMethod {
1801            recv: recv.clone(),
1802            name: name.to_string(),
1803        })
1804    })
1805}
1806
1807/// `Object.prototype` methods reachable on any object.
1808fn is_object_method(name: &str) -> bool {
1809    matches!(
1810        name,
1811        "hasOwnProperty"
1812            | "isPrototypeOf"
1813            | "propertyIsEnumerable"
1814            | "toString"
1815            | "toLocaleString"
1816            | "valueOf"
1817            | "constructor"
1818            | "__defineGetter__"
1819            | "__defineSetter__"
1820            | "__lookupGetter__"
1821            | "__lookupSetter__"
1822    )
1823}
1824
1825/// The `Object.prototype` methods installed as thunks on the real
1826/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1827/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1828pub const OBJECT_PROTO_METHODS: &[&str] = &[
1829    "hasOwnProperty",
1830    "isPrototypeOf",
1831    "propertyIsEnumerable",
1832    "toString",
1833    "toLocaleString",
1834    "valueOf",
1835    "__defineGetter__",
1836    "__defineSetter__",
1837    "__lookupGetter__",
1838    "__lookupSetter__",
1839];
1840
1841/// A typed array with elements cannot be frozen or sealed: its indices are
1842/// non-configurable by construction, so making them non-writable would violate
1843/// the invariant, and node refuses outright rather than half-applying it. An
1844/// EMPTY view and a `DataView` are both fine.
1845/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1846///
1847/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1848/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1849/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1850/// never saw the query.
1851fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1852    if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1853        return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1854    }
1855    // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1856    if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1857        return Ok(Value::Bool(false));
1858    }
1859    for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1860        let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1861            continue;
1862        };
1863        let flag = |name: &str| {
1864            with_host(|h| match h.get(&d) {
1865                Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1866                _ => false,
1867            })
1868        };
1869        let is_data = with_host(
1870            |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1871        );
1872        if flag("configurable") || (freeze && is_data && flag("writable")) {
1873            return Ok(Value::Bool(false));
1874        }
1875    }
1876    Ok(Value::Bool(true))
1877}
1878
1879/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1880/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1881/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1882/// OBJECT, so the handler never saw the operation and the target was untouched.
1883///
1884/// Returns false for a non-proxy, which takes the ordinary path.
1885fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1886    if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1887        return Ok(false);
1888    }
1889    if !crate::proxy::prevent_extensions(v)? {
1890        return Err(host::type_error("Object.freeze called on non-object"));
1891    }
1892    let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1893    for key in keys {
1894        // SEALING asks for no descriptor at all — it only strips
1895        // `configurable`, which is the same for a data property and an
1896        // accessor. FREEZING has to know which it is, because only a data
1897        // property has a `writable` to strip, and that is the one extra trap
1898        // call node makes.
1899        let accessor = if freeze {
1900            let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1901                continue;
1902            };
1903            with_host(
1904                |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1905            )
1906        } else {
1907            false
1908        };
1909        let desc = with_host(|h| {
1910            let mut m: IndexMap<String, Value> = IndexMap::new();
1911            m.insert("configurable".into(), Value::Bool(false));
1912            if freeze && !accessor {
1913                m.insert("writable".into(), Value::Bool(false));
1914            }
1915            h.new_object(m)
1916        });
1917        if !crate::proxy::define_property(v, &key, &desc)? {
1918            return Err(host::type_error(&format!(
1919                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1920            )));
1921        }
1922    }
1923    Ok(true)
1924}
1925
1926fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1927    let has_elements = matches!(
1928        crate::stdlib::native_tag(v).as_deref(),
1929        Some("TypedArray") | Some("Buffer")
1930    ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1931    if has_elements {
1932        return Err(host::type_error(&format!(
1933            "Cannot {verb} array buffer views with elements"
1934        )));
1935    }
1936    Ok(())
1937}
1938
1939pub fn is_object_builtin_method(name: &str) -> bool {
1940    matches!(
1941        name,
1942        "hasOwnProperty"
1943            | "isPrototypeOf"
1944            | "propertyIsEnumerable"
1945            | "toString"
1946            | "toLocaleString"
1947            | "valueOf"
1948            | "__defineGetter__"
1949            | "__defineSetter__"
1950            | "__lookupGetter__"
1951            | "__lookupSetter__"
1952    )
1953}
1954
1955/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1956/// 20.1.3.6, the hook by which a class names its own brand.
1957///
1958/// A Proxy has no chain to probe: the step is an unconditional
1959/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1960/// ordinary receiver does, to keep the read off objects that carry no tag)
1961/// would always miss and brand every tagged proxy `[object Object]`.
1962///
1963/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1964fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1965    let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1966        || with_host(|h| {
1967            host::lookup_chain(h, recv, "@@toStringTag").is_some()
1968                || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1969        });
1970    if !tagged {
1971        return Ok(None);
1972    }
1973    let t = get_property(recv, "@@toStringTag")?;
1974    Ok(with_host(|h| h.as_str(&t)))
1975}
1976
1977/// Dispatch an `Object.prototype` builtin method on an object/instance.
1978pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1979    match name {
1980        // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1981        // reached by pre-`defineProperty` libraries; all four were missing, so
1982        // `o.__defineGetter__` threw "is not a function".
1983        "__defineGetter__" | "__defineSetter__" => {
1984            let getter = name == "__defineGetter__";
1985            let f = args.get(1).cloned().unwrap_or(Value::Undef);
1986            if !with_host(|h| host::is_callable(h, &f)) {
1987                return Err(host::type_error(&format!(
1988                    "Object.prototype.{name}: Expecting function"
1989                )));
1990            }
1991            let key = host::to_property_key(&arg0(&args))?;
1992            let desc = with_host(|h| {
1993                let mut m: IndexMap<String, Value> = IndexMap::new();
1994                m.insert(if getter { "get" } else { "set" }.into(), f);
1995                m.insert("enumerable".into(), Value::Bool(true));
1996                m.insert("configurable".into(), Value::Bool(true));
1997                h.new_object(m)
1998            });
1999            apply_descriptor(recv, &key, &desc)?;
2000            Ok(Value::Undef)
2001        }
2002        "__lookupGetter__" | "__lookupSetter__" => {
2003            let want_get = name == "__lookupGetter__";
2004            let key = host::to_property_key(&arg0(&args))?;
2005            // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
2006            let found = with_host(|h| host::lookup_accessor(h, recv, &key));
2007            Ok(match found {
2008                Some((g, st)) => {
2009                    let side = if want_get { g } else { st };
2010                    side.unwrap_or(Value::Undef)
2011                }
2012                None => Value::Undef,
2013            })
2014        }
2015        "hasOwnProperty" => {
2016            let k = host::to_property_key(&arg0(&args))?;
2017            // The global object OWNS its lazily-bound builtins and every global
2018            // a script created; neither lives in its property map.
2019            if with_host(|h| h.is_global_object(recv))
2020                && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2021                && global_object_binding(&k).is_some()
2022            {
2023                return Ok(Value::Bool(true));
2024            }
2025            // A builtin namespace/prototype receiver (`Map.prototype`) reports
2026            // ownership via `has_property` (its methods resolve as thunks).
2027            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2028                return Ok(Value::Bool(has_property(recv, &k)?));
2029            }
2030            // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2031            // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2032            // the target's property map.
2033            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2034                let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2035                return Ok(Value::Bool(!matches!(d, Value::Undef)));
2036            }
2037            // A Buffer's / typed array's own keys are its element indices: the
2038            // `length`/`byteLength` slots are internal bookkeeping, and V8
2039            // reports `hasOwnProperty('length')` as false for a typed array.
2040            // Shared with the `in` operator so the two cannot drift apart.
2041            if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2042                return Ok(Value::Bool(hit));
2043            }
2044            // A function's `length`/`name`/`prototype` and a RegExp's
2045            // `lastIndex` are SYNTHESIZED own properties: they read back but
2046            // own no map entry, so this answered false where node says true.
2047            if synthesized_own_descriptor(recv, &k).is_some() {
2048                return Ok(Value::Bool(true));
2049            }
2050            if uses_side_table(recv) {
2051                return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2052            }
2053            let has = with_host(|h| match h.get(recv) {
2054                Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2055                Some(JsObj::Array(items)) => {
2056                    k == "length"
2057                        || k.parse::<usize>()
2058                            .map(|i| i < items.len() && !h.is_hole(recv, i))
2059                            .unwrap_or(false)
2060                }
2061                _ => false,
2062            });
2063            Ok(Value::Bool(has))
2064        }
2065        "isPrototypeOf" => {
2066            let target = arg0(&args);
2067            // The ARGUMENT is what gets walked, so a proxy there needs its
2068            // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2069            // proxy does not hold, which reported `false` for every proxy. From
2070            // the second hop on the chain is ordinary objects again, walked by
2071            // the recorded link exactly as before.
2072            let mut cur = match crate::proxy::get_prototype_of(&target)? {
2073                Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
2074                None => with_host(|h| h.proto_of(&target)),
2075            };
2076            while let Some(p) = cur {
2077                if with_host(|h| h.strict_eq(&p, recv)) {
2078                    return Ok(Value::Bool(true));
2079                }
2080                cur = with_host(|h| h.proto_of(&p));
2081            }
2082            Ok(Value::Bool(false))
2083        }
2084        "propertyIsEnumerable" => {
2085            let k = with_host(|h| h.str_of(&arg0(&args)));
2086            // Own *and* enumerable — a non-enumerable own slot reads false. On a
2087            // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2088            // trap, since there is no property map to enumerate.
2089            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2090                let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2091                return Ok(Value::Bool(has));
2092            }
2093            let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2094            Ok(Value::Bool(has))
2095        }
2096        "toString" => {
2097            // An instance with a custom `toString` up the chain is handled by
2098            // call_method before reaching here; this is the default — and the
2099            // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2100            // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2101            // tagged object branded itself `[object T]` when asked one way and
2102            // `[object Object]` when converted the other (`String(o)`, `${o}`,
2103            // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2104            if let Some(t) = to_string_tag(recv)? {
2105                return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2106            }
2107            Ok(with_host(|h| {
2108                let s = h.str_of(recv);
2109                h.new_str(s)
2110            }))
2111        }
2112        // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2113        // `Invoke(this, "toString")` — no locale behavior of its own. It was
2114        // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2115        // calling it threw `is not a function` on every plain object.
2116        "toLocaleString" => {
2117            let v = host::call_method(recv, "toString", Vec::new())?;
2118            Ok(v)
2119        }
2120        "valueOf" => Ok(recv.clone()),
2121        _ => Err(host::type_error(&format!("{name} is not a function"))),
2122    }
2123}
2124
2125/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2126/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2127/// these (so the caller can try statics).
2128pub fn function_builtin_method(
2129    recv: &Value,
2130    name: &str,
2131    args: &[Value],
2132) -> Result<Option<Value>, String> {
2133    match name {
2134        "call" => {
2135            let this = args.first().cloned();
2136            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2137            Ok(Some(host::invoke(recv, rest, this)?))
2138        }
2139        "apply" => {
2140            let this = args.first().cloned();
2141            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2142            // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2143            // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2144            // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2145            // this is written for, and both produced an empty list. A nullish
2146            // second argument means no arguments at all.
2147            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2148                Vec::new()
2149            } else {
2150                create_list_from_array_like(&arr)?
2151            };
2152            Ok(Some(host::invoke(recv, call_args, this)?))
2153        }
2154        "bind" => {
2155            let this = args.first().cloned().unwrap_or(Value::Undef);
2156            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2157            Ok(Some(with_host(|h| {
2158                h.alloc(JsObj::BoundFunc {
2159                    target: recv.clone(),
2160                    this,
2161                    args: pre,
2162                })
2163            })))
2164        }
2165        "toString" => Ok(Some(with_host(|h| {
2166            let s = h.str_of(recv);
2167            h.new_str(s)
2168        }))),
2169        _ => Ok(None),
2170    }
2171}
2172
2173fn is_function_method(name: &str) -> bool {
2174    matches!(name, "call" | "apply" | "bind" | "toString")
2175}
2176fn is_map_method(name: &str) -> bool {
2177    matches!(
2178        name,
2179        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2180    )
2181}
2182fn is_set_method(name: &str) -> bool {
2183    matches!(
2184        name,
2185        "add"
2186            | "has"
2187            | "delete"
2188            | "clear"
2189            | "forEach"
2190            | "keys"
2191            | "values"
2192            | "entries"
2193            | "union"
2194            | "intersection"
2195            | "difference"
2196            | "symmetricDifference"
2197            | "isSubsetOf"
2198            | "isSupersetOf"
2199            | "isDisjointFrom"
2200    )
2201}
2202fn is_generator_method(name: &str) -> bool {
2203    matches!(name, "next" | "return" | "throw")
2204}
2205
2206/// A property read on a function/class value: own fn-props (statics, name,
2207/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2208fn function_property(recv: &Value, name: &str) -> Value {
2209    // A class static, inherited down the constructor chain.
2210    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2211        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2212            return v;
2213        }
2214        // A class's own `name` and `length` are its own, not the builtin
2215        // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2216        // `A.length === 0`, but both were read off `Array`. Only a class that
2217        // WOULD fall through to an ancestor takes this path; a plain class keeps
2218        // the ordinary computation below.
2219        if matches!(name, "name" | "length")
2220            && with_host(|h| h.class_static(recv, name)).is_none()
2221            && with_host(|h| h.class_builtin_ancestor(recv))
2222                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2223        {
2224            if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2225                return v;
2226            }
2227            if name == "name" {
2228                let n = with_host(|h| h.callable_name(recv));
2229                return with_host(|h| h.new_str(n));
2230            }
2231            // The class's own constructor decides its arity; with no explicit
2232            // one the implicit `constructor(...args)` has length 0.
2233            let ctor = with_host(|h| match h.get(recv) {
2234                Some(JsObj::Class(c)) => c.ctor.clone(),
2235                _ => None,
2236            });
2237            return match ctor {
2238                Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2239                None => Value::Float(0.0),
2240            };
2241        }
2242        // `Symbol.species` is an accessor returning `this`, so a subclass that
2243        // does not override it IS its own species. Reading it off the builtin
2244        // ancestor below would answer with the ancestor — `A[Symbol.species]`
2245        // came back as `Array`, which sent every derived result to a plain
2246        // array.
2247        if name == "@@species"
2248            && with_host(|h| h.class_static(recv, "@@species")).is_none()
2249            && with_host(|h| h.class_builtin_ancestor(recv))
2250                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2251        {
2252            return recv.clone();
2253        }
2254        // The chain may bottom out in a BUILTIN constructor (`class D extends
2255        // Array {}`), whose statics `class_static` cannot see — it only walks
2256        // `ClassVal.parent` links between user classes. Finish the lookup with an
2257        // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2258        if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2259            if let Ok(v) = get_property(&anc, name) {
2260                if !matches!(v, Value::Undef) {
2261                    return v;
2262                }
2263            }
2264        }
2265    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2266        return v;
2267    }
2268    // A method inherited via the function's [[Prototype]] chain (set with
2269    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2270    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2271    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2272        return v;
2273    }
2274    match name {
2275        "name" => with_host(|h| {
2276            let n = h.callable_name(recv);
2277            h.new_str(n)
2278        }),
2279        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2280        "prototype" => ensure_fn_prototype(recv),
2281        _ if is_function_method(name) => bound_method(recv, name),
2282        _ => Value::Undef,
2283    }
2284}
2285
2286/// The `.prototype` of a function value, auto-created on first access (as Node
2287/// does for every non-arrow function) with `.constructor` linking back. Arrow
2288/// functions have no `prototype`.
2289fn ensure_fn_prototype(recv: &Value) -> Value {
2290    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2291        return p;
2292    }
2293    // Only a constructor gets one: an arrow, a method definition and an async
2294    // function are not constructors, and a class sets its own (10.2.5).
2295    if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2296        return Value::Undef;
2297    }
2298    if !with_host(|h| h.owns_prototype(recv)) {
2299        return Value::Undef;
2300    }
2301    with_host(|h| {
2302        let proto = h.new_object(IndexMap::new());
2303        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2304            p.insert("constructor".to_string(), recv.clone());
2305        }
2306        h.hide_prop(&proto, "constructor");
2307        h.set_fn_prop(recv, "prototype", proto.clone());
2308        proto
2309    })
2310}
2311
2312/// The numeric constants a core namespace owns, in the order node reports them
2313/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2314/// list: the enumeration and the read have to agree, and they did not — every
2315/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2316/// all eight of Math's, so a member that plainly exists was invisible to any
2317/// reflective copy of the namespace.
2318///
2319/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2320/// is what separates them from the methods alongside them.
2321pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2322    const MATH: &[(&str, f64)] = &[
2323        ("E", std::f64::consts::E),
2324        ("LN10", std::f64::consts::LN_10),
2325        ("LN2", std::f64::consts::LN_2),
2326        ("LOG10E", std::f64::consts::LOG10_E),
2327        ("LOG2E", std::f64::consts::LOG2_E),
2328        ("PI", std::f64::consts::PI),
2329        ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2330        ("SQRT2", std::f64::consts::SQRT_2),
2331    ];
2332    const NUMBER: &[(&str, f64)] = &[
2333        ("MAX_VALUE", f64::MAX),
2334        // The smallest positive value a Number can hold, which is the
2335        // smallest SUBNORMAL double (`5e-324`), not Rust's
2336        // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2337        // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2338        // large.
2339        // The literal, not `f64::from_bits(1)`: that is only const-callable from
2340        // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2341        // bit pattern — the smallest positive subnormal.
2342        ("MIN_VALUE", 5e-324),
2343        ("NaN", f64::NAN),
2344        ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2345        ("POSITIVE_INFINITY", f64::INFINITY),
2346        ("MAX_SAFE_INTEGER", 9007199254740991.0),
2347        ("MIN_SAFE_INTEGER", -9007199254740991.0),
2348        ("EPSILON", f64::EPSILON),
2349    ];
2350    match ns {
2351        "Math" => MATH,
2352        "Number" => NUMBER,
2353        _ => &[],
2354    }
2355}
2356
2357/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2358/// measured on node v26.8.1:
2359///
2360/// ```text
2361/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype   w=false e=false c=false
2362/// Math.max.name, Math.max.length                       w=false e=false c=true
2363/// Math.floor, Array.from, Array.prototype.slice        w=true  e=false c=true
2364/// require('path').join                                 w=true  e=true  c=true
2365/// ```
2366///
2367/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2368/// `name`/`length` is read-only but configurable, and everything else is an
2369/// ordinary method — enumerable exactly when the namespace enumerates it, which
2370/// is what separates a core module's exports from an ECMAScript namespace's.
2371fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2372    let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2373        || key == "prototype"
2374        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2375    let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2376    // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2377    // data property, whatever the namespace's built-in members look like — the
2378    // synthesized answer reported it non-enumerable, so a monkey-patched member
2379    // described itself as one of the intrinsics.
2380    let assigned = !intrinsic_proto_member(ns, key)
2381        && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2382        && with_host(|h| h.builtin_static(ns, key).is_some());
2383    let enumerable = assigned
2384        || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2385    with_host(|h| {
2386        let mut m: IndexMap<String, Value> = IndexMap::new();
2387        m.insert("value".into(), value);
2388        m.insert(
2389            "writable".into(),
2390            Value::Bool(assigned || (!frozen && !own_fn_meta)),
2391        );
2392        m.insert("enumerable".into(), Value::Bool(enumerable));
2393        m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2394        h.new_object(m)
2395    })
2396}
2397
2398/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2399/// [`builtin_member_descriptor`], split out so `delete` can ask without
2400/// building a descriptor object.
2401/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2402/// really defines — as opposed to a name a script added. An assignment over one
2403/// of these is a `[[Set]]` and leaves its attributes alone.
2404fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2405    intrinsic_proto_members(ns).is_some_and(|members| {
2406        members
2407            .iter()
2408            .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2409    })
2410}
2411
2412fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2413    !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2414        || key == "prototype"
2415        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2416}
2417
2418/// The value of `<ns>.<name>` when it is one of those constants.
2419fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2420    namespace_constants(ns)
2421        .iter()
2422        .find(|(k, _)| *k == name)
2423        .map(|(_, v)| *v)
2424}
2425
2426/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2427/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2428/// ECMAScript builtin defines. The generated member table records the same
2429/// distinction with its `+` prefix.
2430fn is_webidl_proto(ctor: &str) -> bool {
2431    intrinsic_proto_members(&format!("{ctor}.prototype"))
2432        .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2433}
2434
2435/// The intrinsic constructor a value's own kind implies — the prototype it
2436/// inherits with no explicit link.
2437pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2438    default_ctor_name(h, v)
2439}
2440
2441/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2442/// an object inheriting it refuses an assignment to that name.
2443pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2444    crate::arity::PROTO_READONLY
2445        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2446        .ok()
2447        .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2448}
2449
2450/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2451/// property or a method.
2452pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2453    crate::arity::PROTO_ACCESSORS
2454        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2455        .ok()
2456        .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2457}
2458
2459/// The constructor whose `.prototype` IS `recv`, whichever of the two
2460/// representations it uses — a `Builtin` namespace handle or a real object.
2461pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2462    with_host(|h| match h.get(recv) {
2463        Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2464        _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2465    })
2466}
2467
2468/// The getter function of an intrinsic prototype accessor, as a first-class
2469/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2470/// hands back, and the form a library uses to borrow one.
2471fn proto_getter(ctor: &str, key: &str) -> Value {
2472    with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2473}
2474
2475/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2476/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2477/// in node even though `Map.prototype` is right there on the chain.
2478fn brand_matches(recv: &Value, ctor: &str) -> bool {
2479    if let Some(tag) = crate::stdlib::native_tag(recv) {
2480        if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2481            return true;
2482        }
2483    }
2484    match ctor {
2485        "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2486        "ArrayBuffer" => with_host(
2487            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2488        ),
2489        _ => {
2490            let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2491                Some(c) => Some(c),
2492                None => with_host(|h| default_ctor_name(h, recv)),
2493            };
2494            own == Some(ctor)
2495        }
2496    }
2497}
2498
2499thread_local! {
2500    /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2501    ///
2502    /// A getter's last step reads the value off the receiver, and when the
2503    /// receiver does not STORE it that read walks the chain, finds the same
2504    /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2505    /// stack overflowed and aborted the process. An accessor already in flight
2506    /// answers `undefined` for its own key rather than re-entering — the value
2507    /// a missing internal slot has, and what node reports for one.
2508    static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2509        const { std::cell::RefCell::new(Vec::new()) };
2510}
2511
2512/// Whether `ctor`'s `key` getter is already running further down the stack.
2513fn getter_in_flight(ctor: &str, key: &str) -> bool {
2514    GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2515}
2516
2517/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2518/// `@protoget:` thunks.
2519///
2520/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2521/// wrong: it answered `undefined` where node runs the getter, fails the brand
2522/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2523/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2524/// that would otherwise throw for every flag reads two of them back.
2525pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2526    let is_the_prototype = with_host(
2527        |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2528    );
2529    if is_the_prototype && ctor == "RegExp" {
2530        // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2531        // the getter returns rather than throwing. `source` and `flags` have
2532        // their own values there; every flag getter answers `undefined`.
2533        return Ok(match key {
2534            "source" => with_host(|h| h.new_str("(?:)".to_string())),
2535            "flags" => with_host(|h| h.new_str(String::new())),
2536            _ => Value::Undef,
2537        });
2538    }
2539    // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2540    // the individual flag properties off whatever object it is handed and
2541    // concatenates their letters, so a plain object answers `""` rather than
2542    // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2543    if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2544        if !with_host(|h| is_object_like(h, recv)) {
2545            return Err(regexp_brand_error(key, recv));
2546        }
2547        let mut out = String::new();
2548        for (prop, letter) in REGEXP_FLAG_LETTERS {
2549            let v = get_property(recv, prop)?;
2550            if with_host(|h| h.truthy(&v)) {
2551                out.push(*letter);
2552            }
2553        }
2554        return Ok(with_host(|h| h.new_str(out)));
2555    }
2556    // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2557    // the getter and the setter throw for every receiver, which is how a strict
2558    // function keeps its caller unreachable. They are not brand checks and do
2559    // not name the receiver.
2560    if ctor == "Function" && matches!(key, "arguments" | "caller") {
2561        return poison_pill_read(recv);
2562    }
2563    if !brand_matches(recv, ctor) {
2564        return Err(match ctor {
2565            "RegExp" => regexp_brand_error(key, recv),
2566            "Symbol" => {
2567                host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2568            }
2569            _ => host::type_error(&format!(
2570                "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2571                brand_receiver_string(recv)
2572            )),
2573        });
2574    }
2575    // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2576    // so that the public name can be a getter on the prototype rather than an
2577    // own enumerable property. Read it straight: the chain walk below would
2578    // find this same accessor and run it again.
2579    if let Some(v) = with_host(|h| match h.get(recv) {
2580        Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2581        _ => None,
2582    }) {
2583        return Ok(v);
2584    }
2585    GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2586    let out = get_property(recv, key);
2587    GETTERS_IN_FLIGHT.with(|g| {
2588        g.borrow_mut().pop();
2589    });
2590    out
2591}
2592
2593/// `Function.prototype.arguments`/`caller` read against `recv`.
2594///
2595/// The pill is conditional and the condition is the RECEIVER, not the reading
2596/// code: a sloppy non-arrow function answers `null` (node stopped populating
2597/// these long ago but kept them readable), and everything else — an arrow, a
2598/// strict function, a non-function — throws. Keying it on the READER's
2599/// strictness, which is what this did, made `strictFn.arguments` answer
2600/// `undefined` from sloppy code and a sloppy function throw from strict code:
2601/// wrong in both directions.
2602pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2603    if with_host(|h| h.fn_is_sloppy(recv)) {
2604        return Ok(with_host(|h| h.null()));
2605    }
2606    Err(host::type_error(POISON_PILL))
2607}
2608
2609/// The message both halves of the `arguments`/`caller` poison pill throw.
2610pub(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";
2611
2612/// How a REJECTED receiver is rendered in a brand-check message.
2613///
2614/// `no_side_effects_string` answers for most of them, but two kinds differ:
2615/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2616/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2617/// that function therefore brands. Node draws the line at whether the value is
2618/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2619/// two cases here are the ones that fall on the other side of it.
2620fn brand_receiver_string(recv: &Value) -> String {
2621    if let Some(ctor) = intrinsic_proto_of(recv) {
2622        return format!("#<{ctor}>");
2623    }
2624    match crate::stdlib::native_tag(recv).as_deref() {
2625        Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2626        _ => no_side_effects_string(recv),
2627    }
2628}
2629
2630/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2631/// concatenates their letters.
2632const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2633    ("hasIndices", 'd'),
2634    ("global", 'g'),
2635    ("ignoreCase", 'i'),
2636    ("multiline", 'm'),
2637    ("dotAll", 's'),
2638    ("unicode", 'u'),
2639    ("unicodeSets", 'v'),
2640    ("sticky", 'y'),
2641];
2642
2643/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2644/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2645/// because 22.2.6.5 reads the individual flags off any object it is given.
2646fn regexp_brand_error(key: &str, recv: &Value) -> String {
2647    if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2648        return host::type_error(&format!(
2649            "RegExp.prototype.flags getter called on non-object {}",
2650            no_side_effects_string(recv)
2651        ));
2652    }
2653    host::type_error(&format!(
2654        "RegExp.prototype.{key} getter called on non-RegExp object"
2655    ))
2656}
2657
2658/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2659/// `console.log`).
2660pub fn namespace_property(ns: &str, name: &str) -> Value {
2661    // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2662    // read sees whatever is loaded now and `delete` (see `delete_property`)
2663    // actually invalidates.
2664    if ns == REQUIRE_CACHE {
2665        return crate::module::cache_get(name).unwrap_or(Value::Undef);
2666    }
2667    // A property a SCRIPT assigned onto this namespace wins over everything
2668    // synthesized below, including a member the namespace really has. That is
2669    // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2670    // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2671    // to read back at all. Only the two `Error` hooks consulted this table, so
2672    // every other assignment onto a builtin — the whole polyfill idiom — was
2673    // stored by `set_property` and then never read: the write appeared to
2674    // succeed, `Object.isExtensible` said true, and the value came back
2675    // `undefined`.
2676    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2677        return v;
2678    }
2679    // The ENTRY script's `require` is this builtin rather than the per-module
2680    // closure, so its `cache` has to be handed out here too.
2681    // `require.extensions` — the legacy loader map. Deprecated but still read
2682    // (and sometimes written) by tooling that hooks module loading, and it was
2683    // absent entirely. The three keys node ships are present; installing a
2684    // custom loader through them is NOT honoured by this runtime's loader, so
2685    // the map reports what it can serve rather than pretending otherwise.
2686    // `util.promisify.custom` — the registered symbol a module attaches to a
2687    // callback function to supply its own promisified form. It was `undefined`,
2688    // so the lookup that decides whether to use one always missed.
2689    if ns == "util.promisify" && name == "custom" {
2690        return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2691    }
2692    // `process.memoryUsage.rss()` — node's fast path for the one figure that
2693    // does not need the whole object built.
2694    if ns == "process.memoryUsage" && name == "rss" {
2695        return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2696    }
2697    if ns == "require" && name == "extensions" {
2698        return with_host(|h| {
2699            let mut m: IndexMap<String, Value> = IndexMap::new();
2700            for ext in [".js", ".json", ".node"] {
2701                let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2702                m.insert(ext.to_string(), f);
2703            }
2704            h.new_object(m)
2705        });
2706    }
2707    // `require.resolve.paths(spec)` — the directories a lookup would search:
2708    // `null` for a core module, the `node_modules` chain otherwise.
2709    if ns == "require.resolve" && name == "paths" {
2710        return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2711    }
2712    if ns == "require" && name == "cache" {
2713        return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2714    }
2715    // The legacy numeric codes `DOMException` carries as statics
2716    // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2717    if ns == "DOMException" {
2718        if let Some((_, code)) = DOM_EXCEPTION_CODES
2719            .iter()
2720            .find(|(n, _)| legacy_code_name(n) == name)
2721        {
2722            return Value::Float(*code);
2723        }
2724    }
2725    // Numeric constants.
2726    if let Some(k) = namespace_constant(ns, name) {
2727        return Value::Float(k);
2728    }
2729    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2730    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2731    // `undefined`.
2732    // `GeneratorFunction.prototype` and the two async variants are REAL objects
2733    // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2734    // sit on the prototype chain of every generator/async function, which a
2735    // handle cannot do. Without this the read fell through to `undefined`.
2736    if name == "prototype"
2737        && matches!(
2738            ns,
2739            "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2740        )
2741    {
2742        return with_host(|h| {
2743            h.ensure_native_protos();
2744            h.native_proto(ns).unwrap_or(Value::Undef)
2745        });
2746    }
2747    // `Error.prepareStackTrace` has a DEFAULT hook in node
2748    // (`ErrorPrepareStackTrace`), so a library probing `if
2749    // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2750    // down the wrong branch. The default renders the header plus the frames,
2751    // which is what the fast path in `materialize_stack` already produces — it
2752    // recognises this exact builtin and skips the round trip.
2753    if ns == "Error" && name == "prepareStackTrace" {
2754        return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2755            || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2756        );
2757    }
2758    // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2759    // lands in the builtin-static side table, which the read below consults
2760    // first. Without a default the READ was `undefined`, so a library doing
2761    // `const old = Error.stackTraceLimit` and restoring it later installed
2762    // `undefined` and disabled the limit permanently.
2763    if ns == "Error" && name == "stackTraceLimit" {
2764        return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2765            .unwrap_or(Value::Float(10.0));
2766    }
2767    // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2768    // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2769    // protocol had nothing to read and every derived result came back a plain
2770    // builtin.
2771    if name == "@@species" && has_species(ns) {
2772        return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2773    }
2774    if name == "name" && is_builtin_ctor(ns) {
2775        return with_host(|h| h.new_str(ns.to_string()));
2776    }
2777    // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2778    // computed property/method key.
2779    if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2780        return with_host(|h| h.well_known_symbol(name));
2781    }
2782    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2783    // `buffer.Buffer`, `url.URL`).
2784    if let Some(v) = crate::stdlib::constant(ns, name) {
2785        return v;
2786    }
2787    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2788    // `Array.prototype`, …): a prototype namespace whose methods are callable
2789    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2790    // `get-intrinsic`/`function-bind` family).
2791    if name == "prototype" && is_builtin_ctor(ns) {
2792        // Same reasoning as the native prototypes below, for the error
2793        // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2794        // `error_protos` object, so `Error.prototype` has to read back that same
2795        // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2796        // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2797        // Error("x")) === Error.prototype` was false, and `typeof
2798        // Error.prototype` was `"function"` where node says `"object"`.
2799        if host::ERROR_NAMES.contains(&ns) {
2800            if let Some(p) = with_host(|h| {
2801                h.ensure_error_protos();
2802                host::error_proto_of(h, ns)
2803            }) {
2804                return p;
2805            }
2806        }
2807        // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2808        // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2809        // Buffer.prototype` must compare equal, which a freshly-allocated
2810        // `Builtin` handle never can.
2811        if let Some(p) = with_host(|h| {
2812            h.ensure_native_protos();
2813            h.native_proto(ns)
2814        }) {
2815            return p;
2816        }
2817        let _ = ns;
2818        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2819    }
2820    // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2821    // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2822    // above never fired and the read produced `undefined` — which broke the ES5
2823    // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2824    // reads `StringDecoder.prototype.end` at load, and threw
2825    // `Cannot read properties of undefined (reading 'end')`. Built from the same
2826    // instance-method table a method read consults, so the two cannot disagree.
2827    if name == "prototype" {
2828        if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2829            return p;
2830        }
2831    }
2832    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2833    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2834    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2835    //
2836    // The thunk is minted only for a name the prototype REALLY carries. Minting
2837    // one unconditionally made every absent name answer with a function:
2838    // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2839    // says `undefined`, and so was every well-known symbol a prototype does not
2840    // define — `Array.prototype[Symbol.toStringTag]` came back a function
2841    // instead of `undefined`, which is a value `Object.prototype.toString` and
2842    // every `typeof`/truthiness test downstream then read wrong.
2843    //
2844    // Existence is decided by the generated intrinsic table, which is read out
2845    // of the reference engine, so this cannot drift from what node defines.
2846    // A name the prototype does not define but `Object.prototype` does is
2847    // INHERITED, and node hands back Object.prototype's own function object
2848    // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2849    // resolves to the `Object` thunk rather than a per-ctor one. That is also
2850    // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2851    // has no own `toString`, and the inherited one is the generic tag reader,
2852    // not a Map method that rejects a non-Map `this`.
2853    if let Some(ctor) = ns.strip_suffix(".prototype") {
2854        // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2855        // not an intrinsic function, so it is not in the arity table the lookup
2856        // above consults. It lists the methods a `with` block must NOT bring
2857        // into scope — the ones added after `with` existed, so old code using a
2858        // variable of the same name keeps working.
2859        if name == "@@unscopables" && ctor == "Array" {
2860            return with_host(|h| {
2861                let mut m: IndexMap<String, Value> = IndexMap::new();
2862                for k in [
2863                    "at",
2864                    "copyWithin",
2865                    "entries",
2866                    "fill",
2867                    "find",
2868                    "findIndex",
2869                    "findLast",
2870                    "findLastIndex",
2871                    "flat",
2872                    "flatMap",
2873                    "includes",
2874                    "keys",
2875                    "toReversed",
2876                    "toSorted",
2877                    "toSpliced",
2878                    "values",
2879                ] {
2880                    m.insert(k.to_string(), Value::Bool(true));
2881                }
2882                let o = h.new_object(m);
2883                let null = h.null();
2884                h.set_proto(&o, null);
2885                o
2886            });
2887        }
2888        if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2889            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2890        }
2891        if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2892            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2893        }
2894        // `constructor` is excluded from the table because it is not a method:
2895        // it is the constructor function itself, and node compares equal
2896        // (`Array.prototype.constructor === Array`). It used to resolve to a
2897        // `@proto:Array:constructor` thunk, which is a different object every
2898        // read and so never compared equal to anything.
2899        if name == "constructor" && is_builtin_ctor(ctor) {
2900            return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2901        }
2902        return Value::Undef;
2903    }
2904    let qualified = format!("{ns}.{name}");
2905    if is_known_builtin(&qualified) {
2906        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2907    }
2908    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2909    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2910        return v;
2911    }
2912    // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2913    // both). `Math.max.name` was `undefined` — as was every `.name` a library
2914    // reads to identify a callback it was handed. The non-callable namespaces
2915    // fall through: `Math.name` and `require('fs').length` really are undefined.
2916    if host::builtin_is_callable(ns) {
2917        match name {
2918            "name" => {
2919                if let Some(n) = proto_getter_name(ns) {
2920                    return with_host(|h| h.new_str(n));
2921                }
2922                return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2923            }
2924            // Only the intrinsics have a specified arity; a core-module
2925            // function's is a property of node's own JS source, so it stays
2926            // `undefined` rather than being invented here.
2927            "length" => {
2928                // A getter takes no argument (10.2.9 / the accessor grammar),
2929                // so its `length` is 0 — it is not in the intrinsic table,
2930                // which holds only named functions.
2931                if proto_getter_name(ns).is_some() {
2932                    return Value::Float(0.0);
2933                }
2934                if let Some((_, len)) = builtin_meta(ns) {
2935                    return Value::Float(len as f64);
2936                }
2937            }
2938            _ => {}
2939        }
2940    }
2941    Value::Undef
2942}
2943
2944/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2945/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2946/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2947/// libraries type-check on; every other method routes through normal method
2948/// dispatch on `recv`.
2949/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2950/// with NO receiver — `const f = [].push; f(1)`.
2951///
2952/// Reading a method off an instance used to mint a thunk bound to that
2953/// instance, so a detached method silently kept working on the object it came
2954/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2955/// to say so. Node words it four ways, and which one a method gets is not
2956/// something that can be derived — the split was measured across every method
2957/// of each prototype:
2958///
2959/// ```text
2960/// ToObject(this)         "Cannot convert undefined or null to object"
2961/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2962/// brand check            "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2963/// everything else        the generic incompatible-receiver message
2964/// ```
2965fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2966    // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2967    // the two `indexOf` family members name themselves, the rest go through
2968    // `ToObject` and report its message.
2969    const ARRAY_NAMED: &[&str] = &[
2970        "concat",
2971        "every",
2972        "filter",
2973        "find",
2974        "findIndex",
2975        "findLast",
2976        "findLastIndex",
2977        "forEach",
2978        "indexOf",
2979        "map",
2980        "reduce",
2981        "reduceRight",
2982        "some",
2983    ];
2984    const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2985    let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2986    let branded =
2987        |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2988    // The generic form names the receiver, so a `null` one must not be reported
2989    // as `undefined`.
2990    let generic = |c: &str, m: &str| {
2991        format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
2992    };
2993    Some(match ctor {
2994        "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
2995        "Array" => TO_OBJECT.to_string(),
2996        // `Object.prototype.toString` is the one method that ACCEPTS a nullish
2997        // receiver — it answers `[object Undefined]`.
2998        "Object" if method == "toString" => return None,
2999        "Object" if method == "toLocaleString" => named("Object"),
3000        "Object" => TO_OBJECT.to_string(),
3001        // Both aliases report the LEGACY name in the message, which is the one
3002        // place `name` and the message disagree.
3003        "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
3004        "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
3005        "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
3006        "String" => named("String"),
3007        "Number" => branded("Number", "Number"),
3008        "Boolean" => branded("Boolean", "Boolean"),
3009        "Symbol" => branded("Symbol", "Symbol"),
3010        "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3011        "Function" if matches!(method, "call" | "apply") => format!(
3012            "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3013        ),
3014        "Function" => branded("Function", "Function"),
3015        // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3016        // a nullish receiver fails inside them and reports that instead.
3017        "Promise" if method == "catch" => {
3018            "Cannot read properties of undefined (reading 'then')".to_string()
3019        }
3020        "Promise" if method == "finally" => {
3021            "Promise.prototype.finally called on non-object".to_string()
3022        }
3023        "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3024        // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3025        // report that slot check; every setter, every `to*String` and the two
3026        // legacy year methods go through the generic receiver check first.
3027        "Date"
3028            if method == "valueOf"
3029                || (method.starts_with("get") && method != "getYear") =>
3030        {
3031            "this is not a Date object.".to_string()
3032        }
3033        // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3034        // and `Set.prototype.keys` IS `values`, one function object each.
3035        "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3036        "Set" if method == "keys" => generic("Set", "values"),
3037        // Everything else that is brand-checked names itself. Node reaches this
3038        // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3039        // is the receiver's kind, and only the message has to agree.
3040        "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3041        | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3042        "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3043        // Node's `URL` methods fail while reaching for their internal state, and
3044        // report the read that failed rather than the method.
3045        "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3046        _ => return None,
3047    })
3048}
3049
3050/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3051/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3052/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3053/// are generic and coerce their receiver with `ToString`.
3054fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3055    match ctor {
3056        "Number" => matches!(
3057            method,
3058            "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3059        ),
3060        "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3061        "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3062        _ => false,
3063    }
3064}
3065
3066/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3067/// primitive itself, the primitive a wrapper boxes, or — for the three
3068/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3069/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3070fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3071    let expected = match ctor {
3072        "Number" => "number",
3073        "String" => "string",
3074        "Boolean" => "boolean",
3075        "BigInt" => "bigint",
3076        _ => return None,
3077    };
3078    let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3079    if is_expected(recv) {
3080        return Some(recv.clone());
3081    }
3082    if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3083        return Some(prim);
3084    }
3085    if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3086        return match ctor {
3087            "Number" => Some(Value::Float(0.0)),
3088            "String" => Some(with_host(|h| h.new_str(""))),
3089            "Boolean" => Some(Value::Bool(false)),
3090            _ => None,
3091        };
3092    }
3093    None
3094}
3095
3096pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3097    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3098    // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3099    // the instance's hidden `@@<name>` slot, which is where the value lives now
3100    // that the public name is a getter rather than an own property.
3101    if let Some(key) = method.strip_prefix("@get@") {
3102        if let Some(v) = with_host(|h| match h.get(recv) {
3103            Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3104            _ => None,
3105        }) {
3106            return Ok(v);
3107        }
3108        // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3109        // `symmetricKeySize` is the secret's byte length, which nothing stores.
3110        let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3111        return crate::stdlib::instance_call(&tag, recv, method, args);
3112    }
3113    if let Some(key) = method.strip_prefix("@set@") {
3114        let v = args.first().cloned().unwrap_or(Value::Undef);
3115        with_host(|h| {
3116            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3117                p.insert(format!("@@{key}"), v);
3118            }
3119        });
3120        crate::stdlib::instance_accessor_written(ctor, key, recv);
3121        return Ok(Value::Undef);
3122    }
3123    if with_host(|h| h.is_nullish(recv)) {
3124        let shown = if with_host(|h| h.is_null(recv)) {
3125            "null"
3126        } else {
3127            "undefined"
3128        };
3129        if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3130            return Err(format!("TypeError: {msg}"));
3131        }
3132    }
3133    // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3134    // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3135    if ctor == "Error" && method == "toString" {
3136        // A `DOMException` keeps its `name`/`message` in internal slots, so the
3137        // chain read below would find the class name on the prototype instead.
3138        if let Some(n) = dom_exception_slot(recv, "name") {
3139            let name = with_host(|h| h.str_of(&n));
3140            let msg = dom_exception_slot(recv, "message")
3141                .map(|m| with_host(|h| h.str_of(&m)))
3142                .unwrap_or_default();
3143            let s = if msg.is_empty() {
3144                name
3145            } else {
3146                format!("{name}: {msg}")
3147            };
3148            return Ok(with_host(|h| h.new_str(s)));
3149        }
3150        // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3151        // so a PROXY supplies them through its `get` trap. Reading the stored
3152        // ones first made `String(new Proxy(err, handler))` ignore the handler.
3153        let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3154        let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3155        let s = match stored.flatten() {
3156            Some(s) => s,
3157            None => {
3158                let read = |k: &str| -> Result<Option<String>, String> {
3159                    Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3160                };
3161                let name = read("name")?.unwrap_or_else(|| "Error".into());
3162                let msg = read("message")?.unwrap_or_default();
3163                if msg.is_empty() {
3164                    name
3165                } else {
3166                    format!("{name}: {msg}")
3167                }
3168            }
3169        };
3170        return Ok(with_host(|h| h.new_str(s)));
3171    }
3172    // The methods that read their receiver through `thisNumberValue` /
3173    // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3174    // primitive, its wrapper, or the prototype object (which carries the zero
3175    // value) — anything else is a TypeError naming the method. Unchecked,
3176    // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3177    // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3178    // recursed through the generic conversion until the stack overflowed.
3179    if is_brand_checked_primitive_method(ctor, method) {
3180        let Some(prim) = this_primitive_value(ctor, recv) else {
3181            return Err(format!(
3182                "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3183            ));
3184        };
3185        return host::call_method(&prim, method, args);
3186    }
3187    // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3188    // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3189    // arrives with an already-primitive receiver and needs no unwrapping.
3190    if matches!(ctor, "String" | "Number" | "Boolean") {
3191        let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3192        return host::call_method(&prim, method, args);
3193    }
3194    // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3195    // readily as the primitive, and neither was unwrapped here. A BigInt
3196    // wrapper's `valueOf` therefore re-entered the generic conversion, which
3197    // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3198    // until the stack overflowed and ABORTED the process, which no try/catch can
3199    // see. A Symbol wrapper failed the brand check below instead and reported
3200    // that `this` was not a Symbol, when it is one. Only a real wrapper is
3201    // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3202    // the brand check.
3203    if matches!(ctor, "Symbol" | "BigInt") {
3204        if let Some(prim) = wrapped_primitive(recv) {
3205            return host::call_method(&prim, method, args);
3206        }
3207    }
3208    if ctor == "Object" && method == "toString" {
3209        // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3210        // (own or inherited, data property or getter) replaces the builtin brand,
3211        // which is how a class advertises its own (`class C { get
3212        // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3213        // runs outside the host borrow so an accessor can be invoked.
3214        // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3215        // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3216        // the ordinary receiver does, to keep the read off objects that have no
3217        // tag) would always miss and brand every tagged proxy `[object Object]`.
3218        if let Some(s) = to_string_tag(recv)? {
3219            return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3220        }
3221        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3222    }
3223    // These thunks now live on the real `Object.prototype` object, i.e. on the
3224    // receiver's own chain — routing back through `call_method` would re-resolve
3225    // this very thunk and recurse.
3226    if ctor == "Object" && is_object_builtin_method(method) {
3227        return object_builtin_method(recv, method, args);
3228    }
3229    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3230    // emitter method directly against `recv` (routing back through `call_method`
3231    // would re-resolve the mixed-in thunk and recurse).
3232    if ctor == "EventEmitter" {
3233        return crate::stdlib::events::instance_call(recv, method, args);
3234    }
3235    // Same recursion hazard for the exotics with a real prototype object: the
3236    // thunk now lives ON the receiver's prototype chain, so `call_method` would
3237    // re-resolve this very thunk. Dispatch straight to the native instance
3238    // implementation when the receiver is in fact an instance of `ctor`.
3239    if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3240        return crate::stdlib::buffer::instance_call(recv, method, &args);
3241    }
3242    // The shared typed-array methods now live on the `%TypedArray%.prototype`
3243    // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3244    // appears for anything read directly off `Uint8Array.prototype`. Both
3245    // dispatch the same way, and both must bypass `call_method` or the thunk
3246    // would re-resolve itself off the receiver's chain and recurse.
3247    if ctor == "Uint8Array" || ctor == "TypedArray" {
3248        match crate::stdlib::native_tag(recv).as_deref() {
3249            Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3250            Some("TypedArray") => {
3251                return crate::stdlib::typedarray::instance_call(recv, method, &args)
3252            }
3253            _ => {}
3254        }
3255    }
3256    // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3257    // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3258    // `LengthOfArrayLike`), which is what makes
3259    // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3260    // is not an Array, so `call_method` would report the method missing.
3261    if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3262        return array_generic(recv, method, args);
3263    }
3264    // The general form of the two special cases above: a thunk taken off a native
3265    // constructor's real prototype, invoked with a receiver that IS an instance of
3266    // that constructor. Routing back through `call_method` would re-resolve this
3267    // very thunk off the receiver's own chain and recurse forever, which is why
3268    // each such prototype needed a hand-written bypass; now they all have one.
3269    // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3270    // through its chain, and requiring an exact tag match sent that call back
3271    // into `call_method`, which re-resolved this same thunk and recursed until
3272    // the stack overflowed.
3273    if let Some(tag) = crate::stdlib::native_tag(recv) {
3274        let mut c = Some(tag.as_str());
3275        while let Some(t) = c {
3276            if t == ctor {
3277                return crate::stdlib::instance_call(&tag, recv, method, args);
3278            }
3279            c = crate::stdlib::native_parent(t);
3280        }
3281    }
3282    // A BRANDED method reached with a receiver that has no such internal slot.
3283    // Every arm above dispatches a receiver that IS an instance, so arriving
3284    // here with one of these constructors means the brand check failed — the
3285    // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3286    // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3287    // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3288    // `union is not a function`, which says the method does not exist rather
3289    // than that the receiver is the wrong kind of object.
3290    // `Date.prototype`'s methods split in two: the ones that read the time value
3291    // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3292    // rest take the ordinary branded form. Measured on node v26.8.1:
3293    // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3294    // and `.setHours.call({})` the second.
3295    if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3296        const THIS_TIME_VALUE: &[&str] = &[
3297            "getTime",
3298            "valueOf",
3299            "getYear",
3300            "getFullYear",
3301            "getMonth",
3302            "getDate",
3303            "getDay",
3304            "getHours",
3305            "getMinutes",
3306            "getSeconds",
3307            "getMilliseconds",
3308            "getUTCFullYear",
3309            "getUTCMonth",
3310            "getUTCDate",
3311            "getUTCDay",
3312            "getUTCHours",
3313            "getUTCMinutes",
3314            "getUTCSeconds",
3315            "getUTCMilliseconds",
3316            "getTimezoneOffset",
3317        ];
3318        if THIS_TIME_VALUE.contains(&method) {
3319            return Err(host::type_error("this is not a Date object."));
3320        }
3321        // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3322        // receiver and INVOKES `toISOString` on it, so it fails on the missing
3323        // method rather than on a brand.
3324        if method != "toJSON" {
3325            return Err(host::type_error(&format!(
3326                "Method Date.prototype.{method} called on incompatible receiver {}",
3327                no_side_effects_string(recv)
3328            )));
3329        }
3330    }
3331    // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3332    // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3333    // check the receiver at the call boundary take the branded form. Measured
3334    // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3335    // genuinely generic (it is `Array.prototype.toString`) and never brands.
3336    if matches!(ctor, "TypedArray" | "Uint8Array")
3337        && !matches!(
3338            crate::stdlib::native_tag(recv).as_deref(),
3339            Some("TypedArray") | Some("Buffer")
3340        )
3341    {
3342        const BRANDED: &[&str] = &[
3343            "slice",
3344            "subarray",
3345            "join",
3346            "sort",
3347            "at",
3348            "toReversed",
3349            "toSorted",
3350            "toLocaleString",
3351        ];
3352        if BRANDED.contains(&method) {
3353            return Err(host::type_error(&format!(
3354                "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3355                no_side_effects_string(recv)
3356            )));
3357        }
3358        // The four base64/hex methods brand themselves against `Uint8Array`
3359        // specifically — a WRONG view is as incompatible as a plain object, and
3360        // the generic guard here cannot tell those apart.
3361        if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3362            return Err(host::type_error(&format!(
3363                "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3364                no_side_effects_string(recv)
3365            )));
3366        }
3367        if method != "toString" {
3368            return Err(host::type_error("this is not a typed array."));
3369        }
3370    }
3371    // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3372    // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3373    // that back to `call_method` read `call` off the proxy again, which
3374    // resolved to this same thunk, and recursed until the stack overflowed and
3375    // aborted the process. The three are defined on the callee alone, so they
3376    // run here: the proxy's `apply` trap (or its target) gets the call.
3377    // `toString` recursed the same way.
3378    if ctor == "Function"
3379        && matches!(method, "call" | "apply" | "bind" | "toString")
3380        && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3381    {
3382        let mut rest = args.into_iter();
3383        let this_arg = rest.next().unwrap_or(Value::Undef);
3384        match method {
3385            "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3386            "apply" => {
3387                let list = match rest.next() {
3388                    None | Some(Value::Undef) => Vec::new(),
3389                    Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3390                    Some(v) => create_list_from_array_like(&v)?,
3391                };
3392                return host::invoke(recv, list, Some(this_arg));
3393            }
3394            "bind" => {
3395                let target = recv.clone();
3396                let pre: Vec<Value> = rest.collect();
3397                return Ok(with_host(|h| {
3398                    h.alloc(JsObj::BoundFunc {
3399                        target,
3400                        this: this_arg,
3401                        args: pre,
3402                    })
3403                }));
3404            }
3405            // A proxy has no source text; V8 prints the native form for it.
3406            _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3407        }
3408    }
3409    // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3410    // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3411    // object. Without this check `Symbol.prototype.toString()` re-entered the
3412    // generic string conversion, which looked `toString` up again and called it
3413    // again — an infinite recursion that overflowed the stack and ABORTED the
3414    // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3415    // The wording is Symbol's own, not the "incompatible receiver" form the
3416    // collections use.
3417    if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3418        // A symbol-KEYED method is named in brackets rather than after a dot:
3419        // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3420        // That is the message `String(Symbol.prototype)` produces, since the
3421        // conversion reaches `@@toPrimitive` before it would reach `toString`.
3422        let named = match method.strip_prefix("@@") {
3423            Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3424            None => format!("Symbol.prototype.{method}"),
3425        };
3426        return Err(host::type_error(&format!(
3427            "{named} requires that 'this' be a Symbol"
3428        )));
3429    }
3430    if let Some(label) = branded_method_label(ctor, recv) {
3431        return Err(host::type_error(&format!(
3432            "Method {label}.prototype.{method} called on incompatible receiver {}",
3433            no_side_effects_string(recv)
3434        )));
3435    }
3436    host::call_method(recv, method, args)
3437}
3438
3439/// The name a branded prototype method reports itself under when its receiver
3440/// fails the brand check, or `None` when `ctor`'s methods are generic over
3441/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3442/// receiver really is an instance.
3443///
3444fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3445    let kind = with_host(|h| h.kind_of(recv));
3446    // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3447    // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3448    // even though both are `JsObj::Set` here.
3449    let weak = peek(recv, |o| match o {
3450        JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3451        _ => None,
3452    })
3453    .unwrap_or(false);
3454    let ok = match ctor {
3455        "Set" => kind == Some(ObjKind::Set) && !weak,
3456        "WeakSet" => kind == Some(ObjKind::Set) && weak,
3457        "Map" => kind == Some(ObjKind::Map) && !weak,
3458        "WeakMap" => kind == Some(ObjKind::Map) && weak,
3459        "Promise" => kind == Some(ObjKind::Promise),
3460        _ => return None,
3461    };
3462    if ok {
3463        return None;
3464    }
3465    Some(match ctor {
3466        "Set" => "Set",
3467        "WeakSet" => "WeakSet",
3468        "Map" => "Map",
3469        "WeakMap" => "WeakMap",
3470        _ => "Promise",
3471    })
3472}
3473
3474/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3475/// uses for a value it must not run user code on. Measured on node v26.8.1
3476/// through `Map.prototype.get.call(x)`:
3477///
3478/// ```text
3479/// 5 / 'str' / true / null / undefined / 9n   the value's own ToString
3480/// Symbol('s')                                Symbol(s)
3481/// function f(){}                             its source text
3482/// new Error('e')                             Error: e
3483/// {} / new (class A {})                      #<Object> / #<A>
3484/// new Map() / Promise.resolve()              #<Map> / #<Promise>
3485/// [] / new Date() / /re/ / new Uint8Array()  [object Array] / [object Date] / …
3486/// { toString() {} } / Object.create(null)    [object Object]
3487/// ```
3488///
3489/// The split is one test: a receiver whose `toString` is still
3490/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3491/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3492/// which is why an object with its own `toString` prints `[object Object]` and
3493/// not what that method returns.
3494fn no_side_effects_string(recv: &Value) -> String {
3495    if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3496        return with_host(|h| h.str_of(recv));
3497    }
3498    if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3499        return s;
3500    }
3501    // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3502    // rather than inside it.
3503    let native = crate::stdlib::native_tag(recv).is_some();
3504    let brands_itself = with_host(|h| {
3505        // `Object.prototype.toString` reaches every object as a thunk on the
3506        // real prototype object, so its presence proves nothing; only a
3507        // toString the receiver's chain OVERRIDES it with counts.
3508        let overridden = host::lookup_chain(h, recv, "toString")
3509            .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3510            .unwrap_or(false);
3511        native
3512            || overridden
3513            || h.has_null_proto(recv)
3514            || !matches!(
3515                h.kind_of(recv),
3516                Some(ObjKind::Object)
3517                    | Some(ObjKind::Map)
3518                    | Some(ObjKind::Set)
3519                    | Some(ObjKind::Promise)
3520            )
3521    });
3522    if brands_itself {
3523        return with_host(|h| object_tag(h, recv));
3524    }
3525    let ctor = get_property(recv, "constructor")
3526        .ok()
3527        .map(|c| with_host(|h| h.callable_name(&c)))
3528        .filter(|n| !n.is_empty())
3529        .unwrap_or_else(|| "Object".to_string());
3530    format!("#<{ctor}>")
3531}
3532
3533/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3534/// one, or `None` when reading that symbol must yield `undefined`.
3535///
3536/// Every builtin brand is already computed in exactly one place (`object_tag`),
3537/// so this reuses it and subtracts the legacy builtins, which brand for
3538/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3539/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3540/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3541/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3542/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3543/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3544/// `TextEncoder`/`TextDecoder` all read their brand.
3545pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3546    // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3547    // of which `object_tag` already brands.
3548    let tag = object_brand(h, v);
3549    const NO_TAG: &[&str] = &[
3550        "Undefined",
3551        "Null",
3552        "Boolean",
3553        "Number",
3554        "String",
3555        "Array",
3556        "Function",
3557        "Object",
3558        "Date",
3559        "RegExp",
3560        "Error",
3561    ];
3562    if NO_TAG.contains(&tag.as_str()) {
3563        return None;
3564    }
3565    Some(tag)
3566}
3567
3568/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3569/// carries an own `Symbol.toStringTag`, if any.
3570fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3571    let mut cur = h.proto_of(v);
3572    for _ in 0..100 {
3573        let p = cur?;
3574        if h.is_null(&p) {
3575            return None;
3576        }
3577        let name = match h.get(&p) {
3578            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3579            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3580        }
3581        // A CLASS prototype is not linked to the builtin its class extends —
3582        // the relationship lives on the class value — so the walk crosses over
3583        // there, or `Object.create(D.prototype)` for `class D extends Map`
3584        // finds nothing.
3585        .or_else(|| {
3586            h.class_owning_proto(&p)
3587                .and_then(|c| h.class_builtin_ancestor(&c))
3588                .map(|b| h.callable_name(&b))
3589                .filter(|n| !n.is_empty())
3590        });
3591        if let Some(n) = name {
3592            if intrinsic_proto_members(&format!("{n}.prototype"))
3593                .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3594            {
3595                return Some(n);
3596            }
3597        }
3598        cur = h.proto_of(&p);
3599    }
3600    None
3601}
3602
3603/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3604/// Every builtin exotic object reports its own brand, which is how packages
3605/// type-test values they did not construct (`toString.call(x) ===
3606/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3607/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3608pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3609    format!("[object {}]", object_brand(h, v))
3610}
3611
3612/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3613/// …), without the `[object …]` wrapper. Split out so the brand and the
3614/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3615/// Whether `v` is a function's `arguments` object.
3616///
3617/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3618/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3619/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3620pub fn is_arguments(v: &Value) -> bool {
3621    with_host(|h| is_arguments_h(h, v))
3622}
3623
3624/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3625/// runs under one, and re-entering through `with_host` aborts the process.
3626pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3627    h.fn_prop(v, "@@arguments").is_some()
3628}
3629
3630fn object_brand(h: &host::JsHost, v: &Value) -> String {
3631    // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3632    // it holds no instance slot, so only the branded few report anything but
3633    // `[object Object]`. Checked before the match because those prototypes are
3634    // plain `JsObj::Object`s and would otherwise be branded by whatever their
3635    // own properties happen to look like — `TypeError.prototype` has `name` and
3636    // `message`, which read as an Error instance.
3637    if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3638        return if BRANDED_PROTOS.contains(&ctor) {
3639            ctor.to_string()
3640        } else {
3641            "Object".to_string()
3642        };
3643    }
3644    let tag: String = match v {
3645        Value::Undef => "Undefined".into(),
3646        Value::Bool(_) => "Boolean".into(),
3647        Value::Int(_) | Value::Float(_) => "Number".into(),
3648        Value::Str(_) => "String".into(),
3649        Value::Obj(_) => match h.get(v) {
3650            Some(JsObj::Null) => "Null".into(),
3651            Some(JsObj::Str(_)) => "String".into(),
3652            Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3653            Some(JsObj::Array(_)) => "Array".into(),
3654            // A lazy iterator helper brands as node does.
3655            Some(JsObj::Object(p))
3656                if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3657            {
3658                "Iterator Helper".into()
3659            }
3660            // A `DOMException` brands by its class, not as a plain `Error`.
3661            Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3662            // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3663            // `Object.prototype.toString.call(new Number(1))` is
3664            // `[object Number]` rather than `[object Object]`.
3665            Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3666                Value::Bool(_) => "Boolean".into(),
3667                Value::Int(_) | Value::Float(_) => "Number".into(),
3668                _ => "String".into(),
3669            },
3670            // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3671            // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3672            // {}))` is `'[object Array]'`. Everything else about a proxy brands
3673            // as a plain Object (a `Symbol.toStringTag` read through the `get`
3674            // trap is handled by the caller, before this).
3675            Some(JsObj::Proxy { target, .. }) => {
3676                let mut cur = target;
3677                for _ in 0..100 {
3678                    match h.get(cur) {
3679                        Some(JsObj::Proxy { target: t, .. }) => cur = t,
3680                        _ => break,
3681                    }
3682                }
3683                match h.get(cur) {
3684                    Some(JsObj::Array(_)) => "Array".into(),
3685                    _ => "Object".into(),
3686                }
3687            }
3688            // `function*` / `async function` / `async function*` carry their own
3689            // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3690            Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3691                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3692                Some(d) if d.is_generator => "GeneratorFunction".into(),
3693                Some(d) if d.is_async => "AsyncFunction".into(),
3694                _ => "Function".into(),
3695            },
3696            // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3697            // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3698            Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3699                n.clone()
3700            }
3701            // A `<Ctor>.prototype` object brands as the constructor it belongs
3702            // to — `Object.prototype.toString.call(Set.prototype)` is
3703            // `[object Set]` — and a `require()`d module namespace is a plain
3704            // object. Neither is a function, so neither brands as one.
3705            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3706                match n.strip_suffix(".prototype") {
3707                    Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3708                    _ => "Object".into(),
3709                }
3710            }
3711            Some(JsObj::Class(_))
3712            | Some(JsObj::Builtin(_))
3713            | Some(JsObj::BoundFunc { .. })
3714            | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3715            // A suspended generator object is `[object Generator]`; an async one
3716            // `[object AsyncGenerator]`.
3717            Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3718            Some(JsObj::Generator { .. }) => "Generator".into(),
3719            Some(JsObj::RegExp(_)) => "RegExp".into(),
3720            Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3721            Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3722            Some(JsObj::Promise { .. }) => "Promise".into(),
3723            Some(JsObj::Symbol { .. }) => "Symbol".into(),
3724            Some(JsObj::BigInt(_)) => "BigInt".into(),
3725            // Native-tagged instances brand by their tag; a typed array brands by
3726            // its element kind (`@@kind`), and every Error subclass is `Error`.
3727            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3728                Some("TypedArray") => p
3729                    .get("@@kind")
3730                    .map(|k| h.str_of(k))
3731                    .unwrap_or_else(|| "Uint8Array".into()),
3732                Some("Buffer") => "Uint8Array".into(),
3733                // Every native class that really carries a `Symbol.toStringTag`
3734                // in Node brands by its own name. Verified against node v26:
3735                // `Object.prototype.toString.call(new WeakRef({}))` is
3736                // `[object WeakRef]`. The rest of the `@@native` tags
3737                // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3738                // classes with NO tag, so they stay `[object Object]` — listing
3739                // them here would invent a brand Node does not have.
3740                Some(
3741                    t @ ("ArrayBuffer"
3742                    | "DataView"
3743                    | "Date"
3744                    | "WeakRef"
3745                    | "FinalizationRegistry"
3746                    | "TextEncoder"
3747                    | "TextDecoder"
3748                    | "URL"
3749                    | "URLSearchParams"),
3750                ) => t.into(),
3751                _ if has_error_data(h, v) => "Error".into(),
3752                _ => "Object".into(),
3753            },
3754            _ => "Object".into(),
3755        },
3756        // node-js only produces the Value variants above; fusevm's shell-oriented
3757        // variants never arise here.
3758        _ => "Object".into(),
3759    };
3760    // Nothing about the value itself brands it. An ordinary object whose CHAIN
3761    // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3762    // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3763    // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3764    // `[object Map]` and was `[object Object]`.
3765    //
3766    // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3767    // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3768    // ACCESSOR returning the specific kind, so consulting the chain FIRST
3769    // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3770    // — three records caught it. `Error.prototype` carries no tag at all, so
3771    // inheriting from it borrows nothing.
3772    if tag == "Object" && !has_error_data(h, v) {
3773        if let Some(ctor) = chain_tag_ctor(h, v) {
3774            return ctor;
3775        }
3776    }
3777    tag
3778}
3779
3780fn b_setattr(vm: &mut VM, _: u8) -> Value {
3781    let val = vm.pop();
3782    let name = sval(&vm.pop());
3783    let recv = vm.pop();
3784    if let Err(e) = set_property(&recv, &name, val.clone()) {
3785        return abort(vm, e);
3786    }
3787    val
3788}
3789
3790/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3791/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3792/// `class C { static [k] = function(){} }`.
3793///
3794/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3795/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3796/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3797/// unconditionally here.
3798///
3799/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3800/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3801fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3802    let func = vm.pop();
3803    let kind = vm.pop().to_int();
3804    let key = vm.pop();
3805    let key = sval(&key);
3806    // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3807    // description gives the empty name, not `[undefined]`.
3808    let base = match with_host(|h| h.symbol_of_key(&key)) {
3809        Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3810            Some(JsObj::Symbol {
3811                desc: Some(desc), ..
3812            }) => format!("[{desc}]"),
3813            _ => String::new(),
3814        },
3815        None => key,
3816    };
3817    let name = match kind {
3818        host::member::GET => format!("get {base}"),
3819        host::member::SET => format!("set {base}"),
3820        _ => base,
3821    };
3822    with_host(|h| {
3823        let s = h.new_str(name);
3824        h.set_fn_prop(&func, "name", s);
3825    });
3826    func
3827}
3828
3829/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3830/// on the same path a plain `o.k = v` takes.
3831pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3832    set_property(recv, name, val)
3833}
3834
3835/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3836/// or `None` when it has none. Reads through a Proxy's
3837/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3838pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3839    let k = with_host(|h| h.new_str(key.to_string()));
3840    let d = own_descriptor_pub(obj, k).ok()?;
3841    if matches!(d, Value::Undef) {
3842        return None;
3843    }
3844    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3845    // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3846    // the host itself, so reading inside the closure double-borrows.
3847    let value = field("value");
3848    let writable = field("writable");
3849    let configurable = field("configurable");
3850    let truthy = |v: &Value| with_host(|h| h.truthy(v));
3851    let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3852    Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3853}
3854
3855/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3856/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3857/// proxy `set` trap forwarding to it both need.
3858///
3859/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3860/// with `receiver` as `this`; a data property does not write to `target` at all
3861/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3862/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3863/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3864/// documented way to forward a write, so the recursion hit every faithful
3865/// handler.
3866pub fn set_with_receiver(
3867    target: &Value,
3868    key: &str,
3869    val: Value,
3870    receiver: &Value,
3871) -> Result<bool, String> {
3872    // A proxy target answers through its own trap, which re-enters here with
3873    // whatever receiver the handler passes on.
3874    if crate::proxy::parts(target).is_some() {
3875        return crate::proxy::set(target, key, &val, receiver);
3876    }
3877    // An accessor anywhere on the target's chain wins, and sees `receiver`.
3878    if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3879        return match setter {
3880            Some(s) => {
3881                host::invoke(&s, vec![val], Some(receiver.clone()))?;
3882                Ok(true)
3883            }
3884            // A getter with no setter refuses the write rather than shadowing it.
3885            None => Ok(false),
3886        };
3887    }
3888    if !with_host(|h| h.can_write_prop(target, key)) {
3889        return Ok(false);
3890    }
3891    // Steps 3.b-3.d: only an object can receive the property, and its OWN
3892    // property decides — an accessor or a read-only slot refuses, and every
3893    // other case defines a plain data property.
3894    //
3895    // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3896    // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3897    // passed them through to `defineProperty`, which then threw `called on
3898    // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3899    if !with_host(|h| is_object_like(h, receiver)) {
3900        return Ok(false);
3901    }
3902    if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3903        if is_accessor || !writable {
3904            return Ok(false);
3905        }
3906    }
3907    // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3908    // by the receiver must not run, and a proxy receiver must reach its
3909    // `defineProperty` trap rather than its `set` trap.
3910    let desc = with_host(|h| {
3911        let mut m: IndexMap<String, Value> = IndexMap::new();
3912        m.insert("value".into(), val);
3913        m.insert("writable".into(), Value::Bool(true));
3914        m.insert("enumerable".into(), Value::Bool(true));
3915        m.insert("configurable".into(), Value::Bool(true));
3916        h.new_object(m)
3917    });
3918    if crate::proxy::parts(receiver).is_some() {
3919        return crate::proxy::define_property(receiver, key, &desc);
3920    }
3921    let k = with_host(|h| h.new_str(key.to_string()));
3922    define_property_pub(receiver, k, desc)?;
3923    Ok(true)
3924}
3925
3926/// Whether the first argument is a PRIMITIVE — including the three that ride as
3927/// heap handles, which a shape test misses.
3928fn is_primitive_arg(args: &[Value]) -> bool {
3929    let v = arg0(args);
3930    with_host(|h| host::is_primitive(h, &v))
3931}
3932
3933/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3934///
3935/// Adding a key to a non-extensible object reports differently from assigning
3936/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3937/// plain object, `[object Array]` for an array.
3938fn write_refused(recv: &Value, name: &str) -> String {
3939    let extensible = with_host(|h| h.is_extensible(recv));
3940    // Which of the two messages applies turns on whether the key already
3941    // EXISTS. Every shape that keeps its own properties in the fn-prop side
3942    // table answered a blanket `true` here, so adding a key to a frozen
3943    // function reported "read only" where node reports "not extensible".
3944    let has_own = with_host(|h| match h.get(recv) {
3945        Some(JsObj::Object(p)) => p.contains_key(name),
3946        Some(JsObj::Array(items)) => {
3947            name.parse::<usize>()
3948                .map(|i| i < items.len())
3949                .unwrap_or(false)
3950                || h.fn_prop(recv, name).is_some()
3951        }
3952        Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3953        _ => h.fn_prop(recv, name).is_some(),
3954    });
3955    if !extensible && !has_own {
3956        return host::type_error(&format!(
3957            "Cannot add property {name}, object is not extensible"
3958        ));
3959    }
3960    // The receiver renders the way every other brand-check message renders one
3961    // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3962    // class instance, `Error: m` for an error. Only Array was special-cased, so
3963    // every other exotic reported `#<Object>`.
3964    host::type_error(&format!(
3965        "Cannot assign to read only property '{name}' of object '{}'",
3966        no_side_effects_string(recv)
3967    ))
3968}
3969
3970fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3971    // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3972    // property of `undefined` or `null` throws, naming the key. Every such
3973    // write was silently discarded, so `u.x = 1` — the mirror of the single
3974    // most common runtime fault in JS, which the READ side already reports —
3975    // looked like it had succeeded.
3976    if with_host(|h| h.is_nullish(recv)) {
3977        return Err(host::type_error(&format!(
3978            "Cannot set properties of {} (setting '{name}')",
3979            with_host(|h| h.str_of(recv))
3980        )));
3981    }
3982    // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
3983    // throwaway wrapper — so it is discarded in sloppy code and throws in
3984    // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
3985    // `is_primitive` rather than a shape test: a string, a symbol and a bigint
3986    // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
3987    // value caught only numbers and booleans.
3988    if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
3989        return Err(host::type_error(&format!(
3990            "Cannot create property '{name}' on {} '{}'",
3991            with_host(|h| h.type_of(recv)),
3992            with_host(|h| h.str_of(recv))
3993        )));
3994    }
3995    // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
3996    // element. The class's own field initializers install theirs directly
3997    // (`host::init_one_field`), so a declaration never reaches this check.
3998    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
3999        return Err(private_brand_message(name, true));
4000    }
4001    // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
4002    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4003        // A `set` trap that returns falsish REFUSED the write: silent in sloppy
4004        // code, a TypeError in strict, exactly as an ordinary refused write is.
4005        if crate::proxy::set(recv, name, &val, recv)? {
4006            return Ok(());
4007        }
4008        if with_host(|h| h.current_strict()) {
4009            return Err(host::type_error(&format!(
4010                "'set' on proxy: trap returned falsish for property '{name}'"
4011            )));
4012        }
4013        return Ok(());
4014    }
4015    // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4016    // back. Writing only the own property left the two views disagreeing:
4017    // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4018    if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4019        with_host(|h| h.set_name(name, val.clone()));
4020    }
4021    // `obj.__proto__ = p` re-links the prototype — but only for the two values
4022    // the Annex B setter accepts, an Object or `null`. Everything else is a
4023    // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4024    // untouched and creates no own key), and a null-prototype object inherits
4025    // no such setter at all, so there the assignment is an ORDINARY own
4026    // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4027    // prototype to the number 5.
4028    if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4029        if with_host(|h| h.has_null_proto(recv)) {
4030            // falls through to the ordinary own-property write below
4031        } else {
4032            let assignable =
4033                with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4034            if assignable {
4035                // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4036                // NON-EXTENSIBLE object refuses — and unlike an ordinary
4037                // refused write, the setter throws in sloppy code too. It was
4038                // rewriting the link of a frozen object.
4039                if would_cycle(recv, &val) {
4040                    return Err(host::type_error("Cyclic __proto__ value"));
4041                }
4042                if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4043                    return Err(host::type_error(&format!(
4044                        "{} is not extensible",
4045                        no_side_effects_string(recv)
4046                    )));
4047                }
4048                with_host(|h| h.set_proto(recv, val));
4049            }
4050            return Ok(());
4051        }
4052    }
4053    // Every environment value is a STRING. `process.env.PORT = 8080` stores
4054    // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4055    // process; storing the number made it add instead.
4056    if !name.starts_with("@@")
4057        && with_host(
4058            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4059        )
4060    {
4061        let text = with_host(|h| h.str_of(&val));
4062        // Write THROUGH to the real environment as well. `process.env` is not a
4063        // private map: node applies the change to the process, so a child
4064        // spawned afterwards inherits it. Keeping it only in the JS object meant
4065        // `process.env.NODE_ENV = 'production'` was invisible to every
4066        // `spawnSync`/`execSync` that followed.
4067        std::env::set_var(name, &text);
4068        let sv = with_host(|h| h.new_str(text));
4069        with_host(|h| {
4070            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4071                p.insert(name.to_string(), sv);
4072            }
4073        });
4074        return Ok(());
4075    }
4076    // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4077    // no later read re-derives a header over the top of the assigned value.
4078    if name == "stack" {
4079        with_host(|h| {
4080            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4081                p.shift_remove("@@stackRaw");
4082            }
4083        });
4084    }
4085    // An inherited/own setter accessor intercepts the write. This is checked
4086    // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4087    // kind first: `writable` is a data-property attribute and means nothing on
4088    // an accessor, where the setter alone decides. Testing it first meant an
4089    // accessor defined through `Object.defineProperty` — which leaves
4090    // `writable` false, having no such field — silently swallowed every write
4091    // instead of calling its setter, so the standard clone idiom
4092    // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4093    // object whose setters did nothing. An accessor from an object literal
4094    // carries all-true attributes, which is why only the former broke.
4095    if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4096        if let Some(setter) = setter {
4097            let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4098            return Ok(());
4099        }
4100        // Only a getter: the write is refused — silent in sloppy mode, a
4101        // TypeError in strict code. The `return` above matters, since a
4102        // successful setter call must not fall into this.
4103        let _ = getter;
4104        if with_host(|h| h.current_strict()) {
4105            return Err(host::type_error(&format!(
4106                "Cannot set property {name} of #<Object> which has only a getter"
4107            )));
4108        }
4109        return Ok(());
4110    }
4111    // A non-writable property, or a new key on a non-extensible object, refuses
4112    // the write. In SLOPPY mode that is silent; in strict code it is a
4113    // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4114    // refusal used to be silent, so `'use strict'` did not catch a write to a
4115    // frozen object, which is most of the reason to freeze one.
4116    if !with_host(|h| h.can_write_prop(recv, name)) {
4117        if with_host(|h| h.current_strict()) {
4118            return Err(write_refused(recv, name));
4119        }
4120        return Ok(());
4121    }
4122    // Writing `name`/`prototype`/statics on a function value.
4123    if matches!(
4124        with_host(|h| h.kind_of(recv)),
4125        Some(ObjKind::Func) | Some(ObjKind::Class)
4126    ) {
4127        with_host(|h| h.set_fn_prop(recv, name, val));
4128        return Ok(());
4129    }
4130    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4131    // Each bare reference is a fresh `Builtin` handle, so route to the stable
4132    // per-namespace side table rather than the per-index `fn_props`.
4133    if let Some(ns) = peek(recv, |o| match o {
4134        JsObj::Builtin(ns) => Some(ns.clone()),
4135        _ => None,
4136    }) {
4137        // `process.exitCode` is an accessor in Node, not a data property: the
4138        // setter validates and stores the code the process will finally exit
4139        // with. Landing it in the generic static table made it a write-only
4140        // decoration — `process.exitCode = 3` read back as 3 and the process
4141        // still exited 0.
4142        if ns == "process" && name == "exitCode" {
4143            return crate::stdlib::process::set_exit_code(&val);
4144        }
4145        with_host(|h| h.set_builtin_static(&ns, name, val));
4146        return Ok(());
4147    }
4148    // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4149    // `String.prototype`, `TypeError.prototype`) is mirrored into the
4150    // per-namespace side table as well as the object's own map. Instances are
4151    // not linked to these objects by `proto_of` — the chain walk never reaches
4152    // them — so the mirror is what makes `String.prototype.pad = f` visible as
4153    // `"x".pad`. The own-map write below still happens, so reading the
4154    // prototype itself and enumerating it keep working unchanged.
4155    if let Some(ns) = with_host(|h| {
4156        h.intrinsic_proto_ctor(recv)
4157            .map(str::to_string)
4158            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4159    }) {
4160        with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4161    }
4162    // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4163    // writability check above already refused it on a FROZEN regexp, which it
4164    // could only do once `integrity_keys` learned that `lastIndex` is an own
4165    // property.
4166    if name == "lastIndex" {
4167        if let Some(n) = with_host(|h| match h.get(recv) {
4168            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4169            _ => None,
4170        }) {
4171            with_host(|h| {
4172                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4173                    r.last_index = if n.is_finite() && n >= 0.0 {
4174                        crate::utf16::U16Index::new(n as usize)
4175                    } else {
4176                        crate::utf16::U16Index::ZERO
4177                    };
4178                }
4179            });
4180            return Ok(());
4181        }
4182    }
4183    // An `arguments` object is an ORDINARY object with a `length` data property,
4184    // not an array: a write PAST the end adds an index and leaves `length`
4185    // alone. The array backing grew it instead, so `f(1)` followed by
4186    // `arguments[1] = 9` reported `arguments.length` as 2.
4187    if let Ok(i) = name.parse::<usize>() {
4188        if is_arguments(recv) && i >= array_len(recv) {
4189            with_host(|h| h.set_fn_prop(recv, name, val));
4190            return Ok(());
4191        }
4192    }
4193    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4194    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4195        let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4196        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4197            return Ok(());
4198        }
4199        // An index write to a view over a DETACHED buffer is DROPPED. Falling
4200        // through would store it as an ordinary own property, which then showed
4201        // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4202        if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4203            return Ok(());
4204        }
4205        // `buf[i] = n` writes through to the Buffer's hidden byte array.
4206        if crate::stdlib::buffer::byte_set(recv, name, &val) {
4207            return Ok(());
4208        }
4209    }
4210    // Any own property on an exotic with no property map of its own. This sits
4211    // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4212    // moves its match cursor rather than being shadowed by a side-table entry.
4213    if uses_side_table(recv) {
4214        with_host(|h| h.set_fn_prop(recv, name, val));
4215        return Ok(());
4216    }
4217    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4218    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4219        && name != "length"
4220        && name.parse::<usize>().is_err()
4221    {
4222        with_host(|h| h.set_fn_prop(recv, name, val));
4223        return Ok(());
4224    }
4225    // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4226    // and does so outside the host borrow because `ToNumber` may run a user
4227    // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4228    let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4229        let want = host::to_array_length(&val)?;
4230        // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4231        // STOPS at the first element that cannot be deleted, leaving the length
4232        // just past it. Truncating regardless discarded a non-configurable
4233        // element and reported a length node would not have accepted.
4234        let floor = with_host(|h| {
4235            let old = match h.get(recv) {
4236                Some(JsObj::Array(items)) => items.len(),
4237                _ => 0,
4238            };
4239            let mut stop = want;
4240            for i in (want..old).rev() {
4241                if !h.prop_attrs(recv, &i.to_string()).configurable {
4242                    stop = i + 1;
4243                    break;
4244                }
4245            }
4246            stop
4247        });
4248        Some(floor.max(want))
4249    } else {
4250        None
4251    };
4252    with_host(|h| match h.get_mut(recv) {
4253        Some(JsObj::Object(props)) => {
4254            // Adding a *new* array-index key must re-place it into ascending
4255            // integer-key order (updating an existing key keeps its position).
4256            let is_new = !props.contains_key(name);
4257            props.insert(name.to_string(), val);
4258            if is_new && host::array_index(name).is_some() {
4259                host::canonicalize_own_keys(props);
4260            }
4261        }
4262        Some(JsObj::Array(items)) => {
4263            if let Some(n) = new_len {
4264                // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4265                // just the one own key); shrinking drops any hole past the end.
4266                let old = items.len();
4267                items.resize(n, Value::Undef);
4268                if n > old {
4269                    h.mark_hole_range(recv, old..n);
4270                } else {
4271                    h.truncate_holes(recv, n);
4272                }
4273            } else if let Ok(i) = name.parse::<usize>() {
4274                // A write PAST the end leaves the skipped positions elided.
4275                let old = items.len();
4276                if i >= old {
4277                    items.resize(i + 1, Value::Undef);
4278                }
4279                items[i] = val;
4280                if i > old {
4281                    h.mark_hole_range(recv, old..i);
4282                }
4283                // …and the written index itself is no longer one. This is the
4284                // single site that keeps a hole record from outliving the
4285                // elision it describes: every array element write in the
4286                // language reaches it.
4287                h.clear_hole(recv, i);
4288            }
4289        }
4290        _ => {}
4291    });
4292    Ok(())
4293}
4294
4295fn b_getitem(vm: &mut VM, _: u8) -> Value {
4296    let idx = vm.pop();
4297    let recv = vm.pop();
4298    let key = match host::to_property_key(&idx) {
4299        Ok(k) => k,
4300        Err(e) => return abort(vm, e),
4301    };
4302    match get_property(&recv, &key) {
4303        Ok(v) => v,
4304        Err(e) => abort(vm, e),
4305    }
4306}
4307
4308fn b_setitem(vm: &mut VM, _: u8) -> Value {
4309    let val = vm.pop();
4310    let idx = vm.pop();
4311    let recv = vm.pop();
4312    let key = match host::to_property_key(&idx) {
4313        Ok(k) => k,
4314        Err(e) => return abort(vm, e),
4315    };
4316    if let Err(e) = set_property(&recv, &key, val.clone()) {
4317        return abort(vm, e);
4318    }
4319    val
4320}
4321
4322/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4323/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4324/// the three cannot drift. Reports `false` for a non-configurable property
4325/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4326/// which is also what deleting an absent key reports.
4327pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4328    // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4329    // `delete u.x` reported success instead.
4330    if with_host(|h| h.is_nullish(recv)) {
4331        return Err(host::type_error(
4332            "Cannot convert undefined or null to object",
4333        ));
4334    }
4335    // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4336    // throw — the reason this reports a `Result` rather than a bare `bool`.
4337    if let Some(b) = crate::proxy::delete(recv, key)? {
4338        return Ok(b);
4339    }
4340    // `delete globalThis.x` removes a global a script created. It lives in the
4341    // globals map, not the object's property map, so the ordinary path reported
4342    // success and removed nothing — the binding stayed readable afterwards.
4343    if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4344        return Ok(true);
4345    }
4346    // `delete require.cache[id]` drops the module so the next `require` of that
4347    // file runs it again — the whole point of exposing the cache.
4348    if peek(recv, |o| match o {
4349        JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4350        _ => None,
4351    }) == Some(true)
4352    {
4353        return Ok(crate::module::cache_delete(key));
4354    }
4355    // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4356    // JS view, so a child spawned afterwards no longer sees it.
4357    if !key.starts_with("@@")
4358        && with_host(
4359            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4360        )
4361    {
4362        std::env::remove_var(key);
4363    }
4364    // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4365    // `Object.prototype`) is non-configurable when it is a constant or a
4366    // constructor's `prototype`, and `delete` of one answers false without
4367    // removing anything. There is no property map behind a namespace, so the
4368    // ordinary attribute lookup below cannot tell — it reported success for
4369    // every one of them.
4370    // A REAL intrinsic prototype object carries the write in its own map AND in
4371    // the side table the instance read consults, so the delete has to clear
4372    // both. Clearing only the map left `Object.prototype.patch` deleted as far
4373    // as the prototype was concerned and still inherited by every object.
4374    if let Some(ns) = with_host(|h| {
4375        h.intrinsic_proto_ctor(recv)
4376            .map(str::to_string)
4377            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4378    }) {
4379        with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4380    }
4381    if let Some(ns) = peek(recv, |o| match o {
4382        JsObj::Builtin(ns) => Some(ns.clone()),
4383        _ => None,
4384    }) {
4385        // A script-assigned static is an ordinary configurable property and is
4386        // removed from the side table the assignment landed in. Falling through
4387        // to the attribute check below answered true and deleted nothing, so a
4388        // patch survived its own `delete`.
4389        if with_host(|h| h.remove_builtin_static(&ns, key)) {
4390            return Ok(true);
4391        }
4392        if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4393            return Ok(false);
4394        }
4395    }
4396    if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4397        return Ok(false);
4398    }
4399    // An accessor lives in its own table, not the property map, so removing it
4400    // has to be explicit — otherwise `delete` reported success while the getter
4401    // kept answering and `in` kept reporting the key.
4402    if with_host(|h| h.own_accessor(recv, key).is_some()) {
4403        with_host(|h| h.remove_accessor(recv, key));
4404        return Ok(true);
4405    }
4406    with_host(|h| {
4407        let index = key.parse::<usize>();
4408        match h.get_mut(recv) {
4409            Some(JsObj::Object(props)) => {
4410                props.shift_remove(key);
4411                return;
4412            }
4413            Some(JsObj::Array(items)) => {
4414                if let Ok(i) = index {
4415                    if i < items.len() {
4416                        // `delete a[i]` punches a HOLE: the length is unchanged
4417                        // but the index stops being an own property.
4418                        items[i] = Value::Undef;
4419                        h.mark_hole(recv, i);
4420                    }
4421                    return;
4422                }
4423            }
4424            _ => {}
4425        }
4426        // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4427        // a function/class, is an ordinary own property kept in the side table.
4428        h.remove_fn_prop(recv, key);
4429    });
4430    Ok(true)
4431}
4432
4433fn b_delitem(vm: &mut VM, _: u8) -> Value {
4434    let strict = vm.pop();
4435    let idx = vm.pop();
4436    let recv = vm.pop();
4437    // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4438    // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4439    // description and delete a key nothing ever wrote.
4440    let key = match host::to_property_key(&idx) {
4441        Ok(k) => k,
4442        Err(e) => return abort(vm, e),
4443    };
4444    match delete_property(&recv, &key) {
4445        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4446            abort(vm, refused_delete_error(&recv, &key))
4447        }
4448        Ok(b) => Value::Bool(b),
4449        Err(e) => abort(vm, e),
4450    }
4451}
4452
4453fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4454    let strict = vm.pop();
4455    let name = sval(&vm.pop());
4456    let recv = vm.pop();
4457    match delete_property(&recv, &name) {
4458        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4459            abort(vm, refused_delete_error(&recv, &name))
4460        }
4461        Ok(b) => Value::Bool(b),
4462        Err(e) => abort(vm, e),
4463    }
4464}
4465
4466/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4467/// receiver renders the way every other brand-check message renders one.
4468fn refused_delete_error(recv: &Value, key: &str) -> String {
4469    // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4470    // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4471    // here rather than in the shared `[[Delete]]`.
4472    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4473        return host::type_error(&format!(
4474            "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4475        ));
4476    }
4477    // A non-callable builtin NAMESPACE renders as a plain object here — node
4478    // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4479    let shown = match peek(recv, |o| match o {
4480        JsObj::Builtin(ns) => Some(ns.clone()),
4481        _ => None,
4482    }) {
4483        Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4484        _ => no_side_effects_string(recv),
4485    };
4486    host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4487}
4488
4489// ── constructors ──────────────────────────────────────────────────────────────
4490
4491fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4492    let parts = pop_n(vm, argc as usize);
4493    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4494    with_host(|h| h.new_str(s))
4495}
4496
4497fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4498    let items = pop_n(vm, argc as usize);
4499    with_host(|h| h.new_array(items))
4500}
4501
4502/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4503/// only for an array literal that actually contains an elision, so a dense
4504/// literal costs nothing. Returns `undefined`; the array stays on the stack
4505/// underneath (the compiler `Dup`s it).
4506fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4507    let idx = vm.pop();
4508    let arr = vm.pop();
4509    let i = match idx {
4510        Value::Int(i) if i >= 0 => i as usize,
4511        _ => return Value::Undef,
4512    };
4513    with_host(|h| h.mark_hole(&arr, i));
4514    Value::Undef
4515}
4516
4517fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4518    let flat = pop_n(vm, argc as usize);
4519    let mut props: IndexMap<String, Value> = IndexMap::new();
4520    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4521    let mut proto_override: Option<Value> = None;
4522    let mut method_keys: Vec<String> = Vec::new();
4523    let mut i = 0;
4524    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4525        if i + 2 >= flat.len() {
4526            break;
4527        }
4528        // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4529        // the literal reserves its slot here with the `@@ord:` marker key that
4530        // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4531        // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4532        // marker landed at the end.
4533        if matches!(flat[i], Value::Int(2)) {
4534            let key = with_host(|h| h.str_of(&flat[i + 1]));
4535            props
4536                .entry(format!("{}{key}", host::ORD_MARKER))
4537                .or_insert(Value::Undef);
4538            i += 3;
4539            continue;
4540        }
4541        // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4542        // recorded so the literal can become its `[[HomeObject]]` below.
4543        if matches!(flat[i], Value::Int(3)) {
4544            let key = with_host(|h| h.str_of(&flat[i + 1]));
4545            method_keys.push(key.clone());
4546            props.insert(key, flat[i + 2].clone());
4547            i += 3;
4548            continue;
4549        }
4550        let spread = matches!(flat[i], Value::Int(1));
4551        if spread {
4552            let src = flat[i + 1].clone();
4553            // A STRING source spreads its index properties (`{..."ab"}` is
4554            // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4555            // String exotic object owns one enumerable property per UTF-16 code
4556            // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4557            // a string source contributed nothing and `{..."ab"}` was `{}`.
4558            // Every other primitive (number/boolean/symbol) boxes to an object
4559            // with no own enumerable properties, and null/undefined are ignored,
4560            // so those correctly stay no-ops on the path below.
4561            if let Some(s) = with_host(|h| h.as_str(&src)) {
4562                for idx in 0..crate::utf16::len(&s) {
4563                    if let Ok(ch) = get_property(&src, &idx.to_string()) {
4564                        props.insert(idx.to_string(), ch);
4565                    }
4566                }
4567                i += 3;
4568                continue;
4569            }
4570            // Object spread copies own *enumerable* properties only — never the
4571            // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4572            // into something that still claimed to be a Buffer) and never a
4573            // property a descriptor marked non-enumerable.
4574            // A getter that throws during spread propagates as a thrown value,
4575            // which in the VM means aborting the frame.
4576            let entries = match host::own_enum_entries_deep(&src) {
4577                Ok(e) => e,
4578                Err(e) => return abort(vm, e),
4579            };
4580            for (k, v) in entries {
4581                props.insert(k, v);
4582            }
4583            // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4584            // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4585            for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4586                props.insert(k, v);
4587            }
4588        } else {
4589            let key = with_host(|h| h.str_of(&flat[i + 1]));
4590            if key == "__proto__" {
4591                proto_override = Some(flat[i + 2].clone());
4592            } else {
4593                props.insert(key, flat[i + 2].clone());
4594            }
4595        }
4596        i += 3;
4597    }
4598    with_host(|h| {
4599        let o = h.new_object(props);
4600        if let Some(p) = proto_override {
4601            if matches!(p, Value::Obj(_)) {
4602                h.set_proto(&o, p);
4603            }
4604        }
4605        // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4606        // is what `super` inside it resolves through. The home object is fixed
4607        // at definition, so a method that merely arrives as a value
4608        // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4609        // method-valued property instead rebound the original and changed what
4610        // IT resolved.
4611        for key in &method_keys {
4612            let m = match h.get(&o) {
4613                Some(JsObj::Object(p)) => p.get(key).cloned(),
4614                _ => None,
4615            };
4616            if let Some(m) = m {
4617                if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4618                    f.home_object = Some(o.clone());
4619                }
4620            }
4621        }
4622        o
4623    })
4624}
4625
4626fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4627    let def_id = match vm.pop() {
4628        Value::Int(n) => n as usize,
4629        Value::Float(f) => f as usize,
4630        _ => return abort(vm, "internal: MKFUNC id".into()),
4631    };
4632    let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4633        Some(d) => (
4634            d.is_arrow,
4635            (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4636        ),
4637        None => (false, None),
4638    });
4639    with_host(|h| {
4640        let mut env = h.current_env_capture();
4641        let this = h.current_this();
4642        // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4643        // exactly as it uses the enclosing `this`. Nothing was captured, so
4644        // `super.m()` inside an arrow reported the method missing — in a class
4645        // method as well as an object literal.
4646        let (home_class, home_static, home_object) = if is_arrow {
4647            h.current_home()
4648        } else {
4649            (None, false, None)
4650        };
4651        // A named function expression closes over an extra scope holding its own
4652        // name, so the body can recurse through it (`function f(){ … f() … }`)
4653        // independently of whatever the outer binding is later set to.
4654        if self_name.is_some() {
4655            env = host::child_env(env);
4656        }
4657        let f = h.alloc(JsObj::Func(FuncVal {
4658            def_id,
4659            env: Some(env.clone()),
4660            this,
4661            is_arrow,
4662            home_class,
4663            home_static,
4664            home_object,
4665        }));
4666        if let Some(n) = self_name {
4667            env.borrow_mut().vars.insert(n, f.clone());
4668        }
4669        f
4670    })
4671}
4672
4673// ── truthiness / coercion / equality ──────────────────────────────────────────
4674
4675fn b_truthy(vm: &mut VM, _: u8) -> Value {
4676    let v = vm.pop();
4677    Value::Bool(with_host(|h| h.truthy(&v)))
4678}
4679
4680fn b_nullish(vm: &mut VM, _: u8) -> Value {
4681    let v = vm.pop();
4682    Value::Bool(with_host(|h| h.is_nullish(&v)))
4683}
4684
4685fn b_tostr(vm: &mut VM, _: u8) -> Value {
4686    let v = vm.pop();
4687    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4688    // `String(x)`, object keys).
4689    match host::to_string_value(&v) {
4690        Ok(s) => s,
4691        Err(e) => abort(vm, e),
4692    }
4693}
4694
4695fn b_typeof(vm: &mut VM, _: u8) -> Value {
4696    let v = vm.pop();
4697    with_host(|h| {
4698        let t = h.type_of(&v);
4699        h.new_str(t)
4700    })
4701}
4702
4703/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4704/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4705fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4706    let name = sval(&vm.pop());
4707    // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4708    // for an UNBOUND name, but a `let` above its declaration is bound and
4709    // throws. Reading the marker's type answered "function".
4710    if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4711        return abort(vm, host::tdz_error(&name));
4712    }
4713    if let Some(v) = with_host(|h| h.read_name(&name)) {
4714        if with_host(|h| h.is_tdz(&v)) {
4715            return abort(vm, host::tdz_error(&name));
4716        }
4717    }
4718    // Bound name (user variable) → typeof its value.
4719    if let Some(v) = with_host(|h| h.read_name(&name)) {
4720        return with_host(|h| {
4721            let t = h.type_of(&v);
4722            h.new_str(t)
4723        });
4724    }
4725    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4726    // would produce, then take its type (so object-namespaces like `console`/
4727    // `Math`/`JSON`/`process` report "object", constructors report "function").
4728    let t = match name.as_str() {
4729        "undefined" => "undefined".to_string(),
4730        "NaN" | "Infinity" => "number".to_string(),
4731        "globalThis" | "global" => "object".to_string(),
4732        n if is_namespace(n) || is_known_builtin(n) => {
4733            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4734            with_host(|h| h.type_of(&v)).to_string()
4735        }
4736        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4737    };
4738    with_host(|h| h.new_str(t))
4739}
4740
4741fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4742    let b = vm.pop();
4743    let a = vm.pop();
4744    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4745}
4746
4747fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4748    let b = vm.pop();
4749    let a = vm.pop();
4750    // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4751    // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4752    // runs before the host borrow. Object ⇄ object stays a reference check.
4753    let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4754        (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4755            Ok(p) => (p, b),
4756            Err(e) => return abort(vm, e),
4757        },
4758        (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4759            Ok(p) => (a, p),
4760            Err(e) => return abort(vm, e),
4761        },
4762        _ => (a, b),
4763    };
4764    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4765}
4766
4767fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4768    let ctor = vm.pop();
4769    let obj = vm.pop();
4770    match host::instance_of(&obj, &ctor) {
4771        Ok(b) => Value::Bool(b),
4772        Err(e) => abort(vm, e),
4773    }
4774}
4775
4776// ── bitwise / unary ───────────────────────────────────────────────────────────
4777
4778fn b_binop(vm: &mut VM, _: u8) -> Value {
4779    let b = vm.pop();
4780    let a = vm.pop();
4781    let tag = match vm.pop() {
4782        Value::Int(n) => n,
4783        _ => 0,
4784    };
4785    // Both operands are ToPrimitive-d with the number hint before ToInt32
4786    // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4787    let r = host::to_primitive(&a, "number")
4788        .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4789        .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4790    finish(vm, r)
4791}
4792
4793fn b_unary(vm: &mut VM, _: u8) -> Value {
4794    let v = vm.pop();
4795    let tag = match vm.pop() {
4796        Value::Int(n) => n,
4797        _ => 0,
4798    };
4799    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4800    // computed in arbitrary precision.
4801    if with_host(|h| h.is_bigint_val(&v)) {
4802        return match tag {
4803            host::unop::POS => abort(
4804                vm,
4805                host::type_error("Cannot convert a BigInt value to a number"),
4806            ),
4807            host::unop::BITNOT => {
4808                let b = with_host(|h| h.as_bigint(&v)).unwrap();
4809                let r = -(b + num_bigint::BigInt::from(1));
4810                with_host(|h| h.new_bigint(r))
4811            }
4812            _ => Value::Undef,
4813        };
4814    }
4815    // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4816    // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4817    let n = match host::to_number_value(&v) {
4818        Ok(n) => n,
4819        Err(e) => return abort(vm, e),
4820    };
4821    match tag {
4822        host::unop::POS => Value::Float(n),
4823        host::unop::BITNOT => {
4824            let i = if n.is_finite() {
4825                n.trunc() as i64 as i32
4826            } else {
4827                0
4828            };
4829            Value::Float(!i as f64)
4830        }
4831        _ => Value::Undef,
4832    }
4833}
4834
4835// ── membership ────────────────────────────────────────────────────────────────
4836
4837fn b_contains(vm: &mut VM, _: u8) -> Value {
4838    let container = vm.pop();
4839    let key = vm.pop();
4840    // `x in y` requires y to be an object. V8 names both operands:
4841    // `Cannot use 'in' operator to search for 'a' in 5`.
4842    // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4843    // `Value::Obj` in this host but is not an object, so the shape test alone
4844    // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4845    // where node throws. `is_primitive` is the same predicate `ToObject` and
4846    // `typeof` use, so the three cannot disagree about what an object is.
4847    if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4848        let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4849        return abort(
4850            vm,
4851            host::type_error(&format!(
4852                "Cannot use 'in' operator to search for '{k}' in {c}"
4853            )),
4854        );
4855    }
4856    let k = match host::to_property_key(&key) {
4857        Ok(k) => k,
4858        Err(e) => return abort(vm, e),
4859    };
4860    match has_property(&container, &k) {
4861        Ok(b) => Value::Bool(b),
4862        Err(e) => abort(vm, e),
4863    }
4864}
4865
4866// ── control ───────────────────────────────────────────────────────────────────
4867
4868fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4869    let v = vm.pop();
4870    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4871    vm.ip = vm.chunk.ops.len();
4872    v
4873}
4874
4875/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4876/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4877/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4878fn b_sig_break(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::Break(label)));
4882    vm.ip = vm.chunk.ops.len();
4883    Value::Undef
4884}
4885
4886/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4887fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4888    let label = sval(&vm.pop());
4889    let label = (!label.is_empty()).then_some(label);
4890    with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4891    vm.ip = vm.chunk.ops.len();
4892    Value::Undef
4893}
4894
4895/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4896/// describes what the `try` is nested in (see [`host::unwind`]):
4897///
4898/// * no signal → `NONE`, execution continues normally;
4899/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4900///   keeps travelling outward;
4901/// * `break`/`continue` targeting the enclosing loop → consume it and report
4902///   `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4903///   continue target;
4904/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4905///   the signal pending, so leaving this loop re-dispatches it one level out.
4906fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4907    let cont_tag = sval(&vm.pop());
4908    let brk_tag = sval(&vm.pop());
4909    let sig = match with_host(|h| h.signal.clone()) {
4910        Some(s) => s,
4911        None => return Value::Int(host::unwind::NONE),
4912    };
4913    // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4914    let propagate = |vm: &mut VM| {
4915        vm.ip = vm.chunk.ops.len();
4916        Value::Int(host::unwind::NONE)
4917    };
4918    match &sig {
4919        host::Signal::Return(_) => propagate(vm),
4920        host::Signal::Break(label) => {
4921            if brk_tag == host::unwind::NO_LOOP {
4922                return propagate(vm);
4923            }
4924            let mine = match label {
4925                None => true, // unlabeled: always the innermost enclosing context
4926                Some(l) => brk_tag == *l,
4927            };
4928            if mine {
4929                with_host(|h| h.signal = None);
4930            }
4931            // Not ours: still leave this context by its break exit, keeping the
4932            // signal pending for the next dispatch point one level out.
4933            Value::Int(host::unwind::BREAK)
4934        }
4935        host::Signal::Continue(label) => {
4936            let mine = match label {
4937                // Unlabeled `continue` binds to the innermost continue-catching
4938                // loop — which a `switch` between here and it is NOT.
4939                None => cont_tag != host::unwind::NO_LOOP,
4940                Some(l) => cont_tag == *l,
4941            };
4942            if mine {
4943                with_host(|h| h.signal = None);
4944                return Value::Int(host::unwind::CONTINUE);
4945            }
4946            if brk_tag == host::unwind::NO_LOOP {
4947                return propagate(vm);
4948            }
4949            // The target loop is further out: exit the innermost context here and
4950            // re-dispatch there.
4951            Value::Int(host::unwind::BREAK)
4952        }
4953    }
4954}
4955
4956fn b_throw(vm: &mut VM, _: u8) -> Value {
4957    let v = vm.pop();
4958    let msg = with_host(|h| {
4959        h.exc = Some(v.clone());
4960        // Prefer an error object's message for the top-level report.
4961        error_display(h, &v)
4962    });
4963    abort(vm, msg)
4964}
4965
4966fn error_display(h: &host::JsHost, v: &Value) -> String {
4967    if let Some(JsObj::Object(props)) = h.get(v) {
4968        let name = props
4969            .get("name")
4970            .map(|x| h.str_of(x))
4971            .unwrap_or_else(|| "Error".into());
4972        if let Some(m) = props.get("message") {
4973            return format!("Uncaught {name}: {}", h.str_of(m));
4974        }
4975    }
4976    format!("Uncaught {}", h.str_of(v))
4977}
4978
4979fn b_try(vm: &mut VM, _: u8) -> Value {
4980    let id = match vm.pop() {
4981        Value::Int(n) => n as usize,
4982        _ => return abort(vm, "internal: TRY id".into()),
4983    };
4984    // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
4985    // its handler and its finalizer bytecode — every time control entered it,
4986    // which for a `try` inside a loop is once per iteration.
4987    let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
4988        Some(t) => t,
4989        None => return abort(vm, "internal: unknown try id".into()),
4990    };
4991    let mut pending: Option<String> = None;
4992    // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
4993    // through can leave block scopes open. Snapshot the scope and restore it
4994    // before the handler and after the whole statement.
4995    let scope = with_host(|h| h.scope_snapshot());
4996
4997    with_host(|h| h.push_scope()); // the try block is its own block scope
4998    let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
4999        with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
5000    });
5001    with_host(|h| h.restore_scope(scope.clone()));
5002    let signal_after = with_host(|h| h.signal.is_some());
5003    if let Err(e) = body_res {
5004        if signal_after {
5005            pending = Some(e);
5006        } else if has_handler {
5007            // Bind the thrown value (or a synthesized error) to the catch param.
5008            let thrown =
5009                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5010            with_host(|h| {
5011                h.error = None;
5012                h.exc = None;
5013            });
5014            // The catch parameter is block-scoped to the handler.
5015            with_host(|h| h.push_scope());
5016            if let Some(name) = &catch_bind {
5017                with_host(|h| h.declare_name(name, thrown));
5018            }
5019            let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5020                with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5021            });
5022            with_host(|h| h.restore_scope(scope.clone()));
5023            if let Err(e2) = hres {
5024                pending = Some(e2);
5025            }
5026        } else {
5027            pending = Some(e);
5028        }
5029    }
5030
5031    // finally always runs; a finally error/signal supersedes.
5032    if has_finalizer {
5033        let sig_before = with_host(|h| h.signal.take());
5034        with_host(|h| h.push_scope()); // ditto for `finally`
5035        let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5036            with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5037        });
5038        with_host(|h| h.restore_scope(scope.clone()));
5039        match fres {
5040            Ok(_) => {
5041                if with_host(|h| h.signal.is_none()) {
5042                    // The finalizer completed normally: the try/catch block's own
5043                    // abrupt completion resumes.
5044                    with_host(|h| h.signal = sig_before);
5045                } else {
5046                    // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5047                    // completion is abrupt (`return`/`break`/`continue` inside
5048                    // `finally`), that completion REPLACES the try/catch block's —
5049                    // including a pending throw, which is discarded, not rethrown.
5050                    pending = None;
5051                    with_host(|h| {
5052                        h.error = None;
5053                        h.exc = None;
5054                    });
5055                }
5056            }
5057            Err(e) => pending = Some(e),
5058        }
5059    }
5060
5061    if let Some(e) = pending {
5062        return abort(vm, e);
5063    }
5064    Value::Undef
5065}
5066
5067/// Synthesize an `Error`-shaped object from an internal error string, linked to
5068/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5069pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5070    h.ensure_error_protos();
5071    // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5072    // it is not one of the ECMAScript error classes below.
5073    if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5074        if let Some((name, msg)) = rest.split_once('\u{1}') {
5075            return dom_exception_with(h, name, msg);
5076        }
5077    }
5078    // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5079    // error class, exactly as Node's internal errors render it in `.stack`.
5080    let (head, rest) = match e.split_once(": ") {
5081        Some((n, m)) => (n, m.to_string()),
5082        None => ("", e.to_string()),
5083    };
5084    let (base, code) = match head.split_once(" [") {
5085        Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5086        _ => (head, None),
5087    };
5088    let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5089        (base.to_string(), rest)
5090    } else {
5091        ("Error".to_string(), e.to_string())
5092    };
5093    // A `host::plain_coded_error` marker: the code rides at the head of the
5094    // MESSAGE rather than in the class, because Node's native-layer errors set
5095    // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5096    // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5097    // internal and must never reach a user-visible `.message`.
5098    let mut code = code;
5099    // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5100    // bracketed head, false for the marker form.
5101    let mut bracketed = code.is_some();
5102    // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5103    let mut fields: Vec<(String, String)> = Vec::new();
5104    if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5105        if let Some((c, m)) = rest.split_once('\u{1}') {
5106            code = Some(c.to_string());
5107            bracketed = false;
5108            let (m, fs) = host::split_error_fields(m);
5109            fields = fs
5110                .into_iter()
5111                .map(|(k, v)| (k.to_string(), v.to_string()))
5112                .collect();
5113            message = m.to_string();
5114        }
5115    }
5116    let mut props: IndexMap<String, Value> = IndexMap::new();
5117    let mv = h.new_str(message.clone());
5118    props.insert("message".into(), mv);
5119    if let Some(c) = &code {
5120        let cv = h.new_str(c.clone());
5121        props.insert("code".into(), cv);
5122        for (k, v) in fields {
5123            let fv = h.new_str(v);
5124            props.insert(k, fv);
5125        }
5126        if bracketed {
5127            // Marks this as a Node JS-layer error, whose `toString` brackets the
5128            // code. A native-layer error has the same `.code` and does not.
5129            props.insert("@@nodeError".into(), Value::Bool(true));
5130        }
5131    }
5132    let label = match (&code, bracketed) {
5133        (Some(c), true) => format!("{name} [{c}]"),
5134        _ => name.clone(),
5135    };
5136    let frames = h.stack_frames();
5137    let stack = if message.is_empty() {
5138        format!("{label}{frames}")
5139    } else {
5140        format!("{label}: {message}{frames}")
5141    };
5142    let sv = h.new_str(stack);
5143    props.insert("stack".into(), sv);
5144    // A libuv system-error message is itself the canonical encoding of the
5145    // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5146    // filesystem/network failure recovers the enumerable `code`/`errno`/
5147    // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5148    // single most common error-handling idiom in Node packages) depend on.
5149    for (k, v) in syscall_error_fields(&message) {
5150        let sv = match v {
5151            SysField::Str(s) => h.new_str(s),
5152            SysField::Num(n) => Value::Float(n),
5153        };
5154        props.insert(k.into(), sv);
5155    }
5156    let obj = h.new_object(props);
5157    if let Some(p) = host::error_proto_of(h, &name) {
5158        h.set_proto(&obj, p);
5159    }
5160    // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5161    // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5162    h.hide_prop(&obj, "message");
5163    h.hide_prop(&obj, "stack");
5164    obj
5165}
5166
5167enum SysField {
5168    Str(String),
5169    Num(f64),
5170}
5171
5172/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5173/// own properties Node hangs off a system error. Returns empty for any message
5174/// that is not in that shape.
5175fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5176    let (code, rest) = match message.split_once(": ") {
5177        Some((c, r))
5178            if c.len() >= 2
5179                && c.starts_with('E')
5180                && c.bytes()
5181                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5182        {
5183            (c, r)
5184        }
5185        _ => return Vec::new(),
5186    };
5187    let mut out: Vec<(&'static str, SysField)> = vec![
5188        ("errno", SysField::Num(errno_for(code))),
5189        ("code", SysField::Str(code.to_string())),
5190    ];
5191    // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5192    if let Some((_, tail)) = rest.split_once(", ") {
5193        let (syscall, path) = match tail.split_once(" '") {
5194            // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5195            // one, so the scan stops at its closing quote rather than at the
5196            // end of the line — which had been swallowing `' -> 'dest` into the
5197            // path for every `rename` and `copyFile` failure.
5198            Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5199            None => (tail, None),
5200        };
5201        out.push(("syscall", SysField::Str(syscall.to_string())));
5202        if let Some(p) = path {
5203            out.push(("path", SysField::Str(p.to_string())));
5204        }
5205    }
5206    out
5207}
5208
5209/// The negative `errno` Node reports for a libuv error code on this platform.
5210/// Only the codes `err_str` can produce are mapped; anything else reports the
5211/// generic `EIO` number rather than inventing a value.
5212fn errno_for(code: &str) -> f64 {
5213    let n: i32 = match code {
5214        "ENOENT" => 2,
5215        "EACCES" => 13,
5216        "EEXIST" => 17,
5217        "ENOTDIR" => 20,
5218        "EISDIR" => 21,
5219        "EINVAL" => 22,
5220        "EPIPE" => 32,
5221        "ENOTEMPTY" => 66,
5222        _ => 5, // EIO
5223    };
5224    -f64::from(n)
5225}
5226
5227// ── iteration ─────────────────────────────────────────────────────────────────
5228
5229fn b_getiter(vm: &mut VM, _: u8) -> Value {
5230    let v = vm.pop();
5231    // A generator is its own iterator (resumed lazily by FORITER).
5232    if with_host(|h| h.is_generator_val(&v)) {
5233        return v;
5234    }
5235    // A Proxy's iterator comes from its traps, materialized eagerly: the
5236    // `lookup_chain` probe below reads the property map a proxy does not have.
5237    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5238        return match crate::proxy::iterate(&v) {
5239            Ok(Some(items)) => with_host(|h| {
5240                h.alloc(JsObj::Iter {
5241                    items,
5242                    idx: 0,
5243                    array: None,
5244                })
5245            }),
5246            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5247            Err(e) => abort(vm, e),
5248        };
5249    }
5250    // Arrays and strings take the direct path below: they have no iterator
5251    // state to preserve and are the hot case, so they must not pay a property
5252    // lookup and a call per loop.
5253    let direct = matches!(
5254        with_host(|h| h.kind_of(&v)),
5255        Some(ObjKind::Array) | Some(ObjKind::Str)
5256    );
5257    // …but only while their `Symbol.iterator` is still reachable. It comes from
5258    // the intrinsic prototype, so replacing the link takes it away: node reports
5259    // `a is not iterable` for an array whose prototype is a plain object, where
5260    // the fast path below iterated the backing vector regardless.
5261    if !own_intrinsic_reachable(&v)
5262        && !matches!(
5263            get_property(&v, "@@iterator"),
5264            Ok(ref f) if with_host(|h| host::is_callable(h, f))
5265        )
5266    {
5267        let shown = with_host(|h| h.inspect(&v));
5268        let msg = host::type_error(&format!("{shown} is not iterable"));
5269        return abort(vm, host::name_call_site(vm, &shown, msg));
5270    }
5271    // Anything else with a `Symbol.iterator`: call it for the iterator object.
5272    //
5273    // Resolved as a full property READ, not a stored-property lookup. A
5274    // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5275    // dispatches its methods through the stdlib table rather than a property
5276    // map, so a `lookup_chain` probe found nothing and the loop fell through to
5277    // materializing the value — which threw for `URLSearchParams` and
5278    // snapshotted for `Map`. Spreading the same object already worked, because
5279    // that path had been fixed and this one had not.
5280    if !direct {
5281        if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5282            if with_host(|h| host::is_callable(h, &iter_fn)) {
5283                return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5284                    Ok(it) => it,
5285                    Err(e) => abort(vm, e),
5286                };
5287            }
5288        }
5289    }
5290    // An array is iterated live, as its `values()` iterator does: a snapshot
5291    // missed every push during the loop, so a worklist `for (const n of q)
5292    // q.push(…)` stopped after the first element.
5293    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Array) {
5294        return array_iterator(&v, host::ArrayIterKind::Values);
5295    }
5296    match with_host(|h| h.iter_vec(&v)) {
5297        Ok(items) => with_host(|h| {
5298            h.alloc(JsObj::Iter {
5299                items,
5300                idx: 0,
5301                array: None,
5302            })
5303        }),
5304        // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5305        // reports `a is not iterable`. The text was recorded for this op.
5306        Err(e) => {
5307            let shown = with_host(|h| h.inspect(&v));
5308            let named = host::name_call_site(vm, &shown, e);
5309            abort(vm, named)
5310        }
5311    }
5312}
5313
5314fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5315    let v = vm.pop();
5316    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5317    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5318    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5319    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5320        // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5321        // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5322        // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5323        // the body the way node's do, instead of all firing up front.
5324        return match crate::proxy::own_keys(&v) {
5325            Ok(keys) => with_host(|h| {
5326                let out: Vec<Value> = keys
5327                    .unwrap_or_default()
5328                    .into_iter()
5329                    .filter(|k| !host::is_symbol_key(k))
5330                    .map(|k| h.new_str(k))
5331                    .collect();
5332                h.new_array(out)
5333            }),
5334            Err(e) => abort(vm, e),
5335        };
5336    }
5337    let mut keys = with_host(|h| h.enum_keys(&v));
5338    // A member patched onto the receiver's INTRINSIC prototype is enumerable
5339    // and inherited, so `for-in` visits it after the own keys — but the
5340    // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5341    // pass never reaches them.
5342    if !with_host(|h| h.has_null_proto(&v)) {
5343        let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5344        for ns in intrinsic_proto_namespaces(&v) {
5345            for k in with_host(|h| h.builtin_static_keys(&ns)) {
5346                if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5347                    keys.push(with_host(|h| h.new_str(k)));
5348                }
5349            }
5350        }
5351    }
5352    with_host(|h| h.new_array(keys))
5353}
5354
5355/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5356/// own constructor's and then `Object`'s, the same two steps
5357/// `inherited_builtin_static` looks a value up in.
5358fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5359    let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5360        Some(c) => Some(c),
5361        None if is_arguments(v) => Some("Object"),
5362        None => with_host(|h| default_ctor_name(h, v)),
5363    };
5364    let mut out: Vec<String> = ctor
5365        .filter(|c| *c != "Object")
5366        .map(|c| format!("{c}.prototype"))
5367        .into_iter()
5368        .collect();
5369    out.push("Object.prototype".to_string());
5370    out
5371}
5372
5373/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5374///
5375/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5376/// of the enumerable keys is observationally the same for everything except
5377/// this), and the body can delete a key before the loop reaches it. Node does
5378/// not visit a key deleted that way; without this check `delete d.z` inside the
5379/// loop still produced `x,y,z`.
5380///
5381/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5382/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5383/// `has` trap, which node never fires for `for-in`. That also puts each trap
5384/// call immediately before its visit, matching node's interleaving — the trap
5385/// log used to show every `gopd` up front because the key list was filtered
5386/// eagerly.
5387fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5388    let key = vm.pop();
5389    let obj = vm.pop();
5390    let name = with_host(|h| h.str_of(&key));
5391    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5392        return match crate::proxy::own_enumerable(&obj, &name) {
5393            Ok(b) => Value::Bool(b),
5394            Err(e) => abort(vm, e),
5395        };
5396    }
5397    // A STRING's keys are its character indices. `in` is not defined on a string
5398    // primitive at all, so the ordinary path below has no answer for one and
5399    // `for (const i in 'abc')` came back empty.
5400    if let Some(s) = with_host(|h| h.as_str(&obj)) {
5401        let len = crate::utf16::len(&s);
5402        return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5403    }
5404    // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5405    // there and does NOT re-check enumerability — making one non-enumerable
5406    // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5407    // (The Proxy branch above does re-check, because there the answer comes from
5408    // the trap node itself calls.)
5409    Value::Bool(has_property_ordinary(&obj, &name))
5410}
5411
5412fn b_foriter(vm: &mut VM, _: u8) -> Value {
5413    let it = match vm.stack.last() {
5414        Some(v) => v.clone(),
5415        None => return abort(vm, "internal: FORITER with empty stack".into()),
5416    };
5417    // A built-in iterator: a snapshot (strings, a Proxy's items) or live over
5418    // an array.
5419    if let Some(step) = iter_step(&it) {
5420        return match step {
5421            Some(v) => {
5422                vm.push(v);
5423                Value::Bool(true)
5424            }
5425            None => Value::Bool(false),
5426        };
5427    }
5428    // Generator: resume one step.
5429    if with_host(|h| h.is_generator_val(&it)) {
5430        return match host::gen_resume(&it, Value::Undef) {
5431            Ok(host::GenStep::Yield(v)) => {
5432                vm.push(v);
5433                Value::Bool(true)
5434            }
5435            Ok(host::GenStep::Done(_)) => Value::Bool(false),
5436            Err(e) => abort(vm, e),
5437        };
5438    }
5439    // A user iterator object with a `.next()` returning `{ value, done }`.
5440    match host::call_method(&it, "next", Vec::new()) {
5441        Ok(step) => {
5442            let done = get_property(&step, "done")
5443                .map(|d| with_host(|h| h.truthy(&d)))
5444                .unwrap_or(true);
5445            if done {
5446                Value::Bool(false)
5447            } else {
5448                match get_property(&step, "value") {
5449                    Ok(v) => {
5450                        vm.push(v);
5451                        Value::Bool(true)
5452                    }
5453                    Err(e) => abort(vm, e),
5454                }
5455            }
5456        }
5457        Err(e) => abort(vm, e),
5458    }
5459}
5460
5461fn b_unpack(vm: &mut VM, _: u8) -> Value {
5462    let star = match vm.pop() {
5463        Value::Int(n) => n,
5464        _ => -1,
5465    };
5466    let count = match vm.pop() {
5467        Value::Int(n) => n as usize,
5468        _ => 0,
5469    };
5470    let iterable = vm.pop();
5471    // Without a `...rest` element the pattern needs exactly `count` values and
5472    // must then close the iterator; draining hung on an unbounded source.
5473    let items = match if star < 0 {
5474        host::iter_take(&iterable, count)
5475    } else {
5476        host::iter_all(&iterable)
5477    } {
5478        Ok(v) => v,
5479        // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5480        // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5481        // was recorded for this op at compile time.
5482        Err(e) => {
5483            // Node names the source only when the pattern's right-hand side is
5484            // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5485            // Anything else (a member, a call, a nested pattern, a parameter)
5486            // reports the TYPE instead, with the property note. Measured across
5487            // twelve shapes rather than guessed.
5488            let msg = match host::call_site_text(vm) {
5489                Some(text) => host::type_error(&format!("{text} is not iterable")),
5490                None if e.ends_with(" is not iterable") => {
5491                    host::type_error(&not_iterable_typed(&iterable))
5492                }
5493                None => e,
5494            };
5495            return abort(vm, msg);
5496        }
5497    };
5498    let ordered: Vec<Value> = if star < 0 {
5499        (0..count)
5500            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5501            .collect()
5502    } else {
5503        let si = star as usize;
5504        let after = count.saturating_sub(si + 1);
5505        let rest_end = items.len().saturating_sub(after).max(si);
5506        let mut out: Vec<Value> = Vec::with_capacity(count);
5507        for i in 0..si {
5508            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5509        }
5510        let rest: Vec<Value> = items
5511            .get(si..rest_end)
5512            .map(|s| s.to_vec())
5513            .unwrap_or_default();
5514        out.push(with_host(|h| h.new_array(rest)));
5515        for j in 0..after {
5516            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5517        }
5518        out
5519    };
5520    if ordered.is_empty() {
5521        return Value::Undef;
5522    }
5523    for it in ordered[1..].iter().rev().cloned() {
5524        vm.push(it);
5525    }
5526    ordered[0].clone()
5527}
5528
5529fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5530    let flat = pop_n(vm, argc as usize);
5531    let mut out = Vec::new();
5532    // Elided positions of an array literal (tag 2), recorded as the run-time
5533    // index each lands on — which only this walk knows, because a preceding
5534    // spread contributes an unknown number of elements. Call-argument lists,
5535    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5536    // empty for them.
5537    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5538    let mut i = 0;
5539    while i + 1 < flat.len() {
5540        let val = flat[i + 1].clone();
5541        match flat[i] {
5542            // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5543            // report a non-iterable differently, which is the only reason the
5544            // two are told apart here.
5545            Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5546                let shown = with_host(|h| h.inspect(&val));
5547                host::name_call_site(vm, &shown, e)
5548            }) {
5549                Ok(items) => out.extend(items),
5550                Err(e) => return abort(vm, e),
5551            },
5552            Value::Int(3) => match host::iter_all(&val) {
5553                Ok(items) => out.extend(items),
5554                Err(e) => {
5555                    // A NULLISH spread names the value and what could not be
5556                    // read off it; anything else names the missing protocol.
5557                    let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5558                    return abort(
5559                        vm,
5560                        match shown {
5561                            Some(s) => host::type_error(&format!(
5562                                "{s} is not iterable (cannot read property {s})"
5563                            )),
5564                            None if e.ends_with(" is not iterable") => host::type_error(
5565                                "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5566                            ),
5567                            None => e,
5568                        },
5569                    );
5570                }
5571            },
5572            Value::Int(2) => {
5573                holes.insert(out.len());
5574                out.push(Value::Undef);
5575            }
5576            _ => out.push(val),
5577        }
5578        i += 2;
5579    }
5580    with_host(|h| {
5581        let arr = h.new_array(out);
5582        h.install_holes(&arr, holes);
5583        arr
5584    })
5585}
5586
5587// ── calls ──────────────────────────────────────────────────────────────────────
5588
5589fn b_call(vm: &mut VM, argc: u8) -> Value {
5590    let mut args = pop_n(vm, argc as usize);
5591    let name = sval(&args.remove(0));
5592    let r = host::call_named(&name, args);
5593    // A bare name that resolved to a non-callable reports the VALUE
5594    // (`undefined is not a function`); node names the identifier. Resolving it
5595    // again to learn what the message said costs nothing off the error path.
5596    let r = r.map_err(|e| {
5597        let shown = global_binding(&name)
5598            .map(|v| with_host(|h| h.str_of(&v)))
5599            .unwrap_or_default();
5600        host::name_call_site(vm, &shown, e)
5601    });
5602    finish(vm, r)
5603}
5604
5605/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5606/// method name. `call_method` resolves by name and bottoms out in
5607/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5608/// that happens to be a function reported "is not a function". Read the element
5609/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5610/// A computed call's key is a property key, so it goes through ToPropertyKey:
5611/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5612/// answers "" for a number, which turned `arr[0]()` into a call to the method
5613/// named "" — so the key is stringified here instead.
5614fn call_key_of(v: &Value) -> String {
5615    if let Value::Str(s) = v {
5616        return (**s).clone();
5617    }
5618    // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5619    // spelling that `str_of` does not produce — it renders
5620    // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5621    // method by that display text and reported it was not a function, for every
5622    // object including a plain literal with a computed symbol method. Reading
5623    // the same property without calling it worked, which is what hid this.
5624    with_host(|h| h.property_key(v))
5625}
5626
5627fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5628    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5629        return None;
5630    }
5631    let f = get_property(recv, name).ok()?;
5632    with_host(|h| host::is_callable(h, &f))
5633        .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5634}
5635
5636fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5637    let mut args = pop_n(vm, argc as usize);
5638    let recv = args.remove(0);
5639    let name = call_key_of(&args.remove(0));
5640    if let Some(r) = index_element_call(&recv, &name, &args) {
5641        return finish(vm, r);
5642    }
5643    let r = host::call_method(&recv, &name, args);
5644    // `z.f()` on a missing method is `z.f is not a function` in node, not
5645    // `f is not a function`: V8 names the callee as the source wrote it. The
5646    // text was recorded for this op at compile time.
5647    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5648    finish(vm, r)
5649}
5650
5651fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5652    let mut args = pop_n(vm, argc as usize);
5653    let callable = args.remove(0);
5654    let r = host::invoke(&callable, args, None);
5655    // The callee here is an expression, not a name, so the message it produced
5656    // describes the VALUE (`undefined is not a function`); node names the
5657    // expression. Same site table, keyed on that rendering.
5658    let r = r.map_err(|e| {
5659        let shown = with_host(|h| h.str_of(&callable));
5660        host::name_call_site(vm, &shown, e)
5661    });
5662    finish(vm, r)
5663}
5664
5665/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5666/// rather than a fixed count of stack slots.
5667///
5668/// `compile_new` used to compile each argument with `compile_expr`, and a
5669/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5670/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5671/// Invalid Date.
5672fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5673    let args_arr = vm.pop();
5674    let ctor = vm.pop();
5675    let args = host::iter_all(&args_arr).unwrap_or_default();
5676    let r = host::construct(&ctor, args).map_err(|e| {
5677        let shown = with_host(|h| h.str_of(&ctor));
5678        host::name_call_site(vm, &shown, e)
5679    });
5680    finish(vm, r)
5681}
5682
5683fn b_new(vm: &mut VM, argc: u8) -> Value {
5684    let mut args = pop_n(vm, argc as usize);
5685    let ctor = args.remove(0);
5686    let r = host::construct(&ctor, args);
5687    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5688    // call does.
5689    let r = r.map_err(|e| {
5690        let shown = with_host(|h| h.str_of(&ctor));
5691        host::name_call_site(vm, &shown, e)
5692    });
5693    finish(vm, r)
5694}
5695
5696fn b_apply(vm: &mut VM, _: u8) -> Value {
5697    let args_arr = vm.pop();
5698    let callable = vm.pop();
5699    let args = host::iter_all(&args_arr).unwrap_or_default();
5700    let r = host::invoke(&callable, args, None);
5701    finish(vm, r)
5702}
5703
5704fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5705    let args_arr = vm.pop();
5706    let name = call_key_of(&vm.pop());
5707    let recv = vm.pop();
5708    let args = host::iter_all(&args_arr).unwrap_or_default();
5709    if let Some(r) = index_element_call(&recv, &name, &args) {
5710        return finish(vm, r);
5711    }
5712    let r = host::call_method(&recv, &name, args);
5713    finish(vm, r)
5714}
5715
5716// ── numeric hook ──────────────────────────────────────────────────────────────
5717
5718/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5719/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5720///
5721/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5722/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5723/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5724/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5725/// the VM and `arith` runs under the host's `RefCell` borrow.
5726pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5727    use NumOp::*;
5728    let (a, b) = match op {
5729        // `==`/`!=` only convert when the OTHER side is a primitive that can be
5730        // compared numerically or textually; `{} == {}` stays a reference check.
5731        Eq | Ne => {
5732            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5733            match (pa, pb) {
5734                (false, true) if coerces_against_object(b) => {
5735                    (host::to_primitive(a, "default")?, b.clone())
5736                }
5737                (true, false) if coerces_against_object(a) => {
5738                    (a.clone(), host::to_primitive(b, "default")?)
5739                }
5740                _ => (a.clone(), b.clone()),
5741            }
5742        }
5743        // `+` uses the default hint (`valueOf` first, but a string result still
5744        // selects concatenation); everything else uses the number hint.
5745        Add => (
5746            host::to_primitive(a, "default")?,
5747            host::to_primitive(b, "default")?,
5748        ),
5749        _ => (
5750            host::to_primitive(a, "number")?,
5751            host::to_primitive(b, "number")?,
5752        ),
5753    };
5754    reject_symbol_operand(op, &a, &b)?;
5755    with_host(|h| h.arith(op, &a, &b))
5756}
5757
5758/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5759/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5760/// concatenated `Symbol(desc)` into the result.
5761///
5762/// Which of the two messages V8 uses is decided by whether the operation is
5763/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5764/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5765/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5766/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5767/// (`Symbol() == 1` is `false`), so they are left alone.
5768fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5769    use NumOp::*;
5770    if matches!(op, Eq | Ne) {
5771        return Ok(());
5772    }
5773    let (sym, concat) = with_host(|h| {
5774        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5775        let is_str =
5776            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5777        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5778    });
5779    if !sym {
5780        return Ok(());
5781    }
5782    Err(host::type_error(if matches!(op, Add) && concat {
5783        "Cannot convert a Symbol value to a string"
5784    } else {
5785        "Cannot convert a Symbol value to a number"
5786    }))
5787}
5788
5789/// Whether a primitive `v` makes `==` against an object convert that object
5790/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5791/// `undefined` and booleans are settled without a `ToPrimitive` call
5792/// (a boolean is coerced to a number first, and then it does).
5793fn coerces_against_object(v: &Value) -> bool {
5794    match v {
5795        Value::Undef => false,
5796        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5797        _ => with_host(|h| !h.is_null(v)),
5798    }
5799}
5800
5801// ══ standard library ═══════════════════════════════════════════════════════════
5802
5803/// Namespaces reachable as bare globals.
5804fn is_namespace(name: &str) -> bool {
5805    matches!(
5806        name,
5807        "console"
5808            | "Math"
5809            | "JSON"
5810            | "Object"
5811            | "Array"
5812            | "Number"
5813            | "String"
5814            | "Boolean"
5815            | "Symbol"
5816            | "Reflect"
5817            | "Promise"
5818            | "process"
5819            | "Buffer"
5820            | "URL"
5821            | "URLSearchParams"
5822    )
5823}
5824
5825const GLOBAL_FUNCS: &[&str] = &[
5826    "parseInt",
5827    "parseFloat",
5828    "isNaN",
5829    "isFinite",
5830    "encodeURIComponent",
5831    "decodeURIComponent",
5832    "encodeURI",
5833    "decodeURI",
5834    // Annex B legacy encoders. Still globals on every engine, and still called
5835    // by pre-`encodeURIComponent` library code.
5836    "escape",
5837    "unescape",
5838    "eval",
5839    "String",
5840    "Number",
5841    "Boolean",
5842    "Array",
5843    "Object",
5844    "Function",
5845    "Symbol",
5846    "Map",
5847    "Set",
5848    "WeakMap",
5849    "WeakSet",
5850    "Promise",
5851    "Error",
5852    "TypeError",
5853    "RangeError",
5854    "SyntaxError",
5855    "ReferenceError",
5856    "EvalError",
5857    "URIError",
5858    "AggregateError",
5859    "DOMException",
5860    "Iterator",
5861    "BigInt",
5862    "RegExp",
5863    "Date",
5864    "ArrayBuffer",
5865    "DataView",
5866    "Uint8Array",
5867    "Int8Array",
5868    "Uint8ClampedArray",
5869    "Int16Array",
5870    "Uint16Array",
5871    "Int32Array",
5872    "Uint32Array",
5873    "Float32Array",
5874    "Float64Array",
5875    "BigInt64Array",
5876    "BigUint64Array",
5877    "WeakRef",
5878    "FinalizationRegistry",
5879    "TextEncoder",
5880    "TextDecoder",
5881    // WHATWG Fetch globals (see `stdlib::fetch`).
5882    "fetch",
5883    "Headers",
5884    "Request",
5885    "Response",
5886    "Blob",
5887    "File",
5888    "FormData",
5889    "AbortController",
5890    "AbortSignal",
5891    "queueMicrotask",
5892    "setTimeout",
5893    "setInterval",
5894    "setImmediate",
5895    "clearTimeout",
5896    "clearInterval",
5897    "clearImmediate",
5898    "structuredClone",
5899    // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5900    // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5901    "btoa",
5902    "atob",
5903    "Proxy",
5904    "require",
5905    // CommonJS loader dispatch targets referenced by per-module `require`
5906    // closures (see `module.rs`); never written by user code.
5907    "__cjs_require",
5908    "__cjs_resolve",
5909    "__cjs_cache",
5910];
5911
5912const NS_METHODS: &[&str] = &[
5913    "console.log",
5914    "console.error",
5915    "console.warn",
5916    "console.info",
5917    "console.debug",
5918    "Math.abs",
5919    "Math.acos",
5920    "Math.acosh",
5921    "Math.asin",
5922    "Math.asinh",
5923    "Math.atan",
5924    "Math.atanh",
5925    "Math.atan2",
5926    "Math.ceil",
5927    "Math.cbrt",
5928    "Math.expm1",
5929    "Math.clz32",
5930    "Math.cos",
5931    "Math.cosh",
5932    "Math.exp",
5933    "Math.floor",
5934    "Math.fround",
5935    "Math.hypot",
5936    "Math.imul",
5937    "Math.log",
5938    "Math.log1p",
5939    "Math.log2",
5940    "Math.log10",
5941    "Math.max",
5942    "Math.min",
5943    "Math.pow",
5944    "Math.random",
5945    "Math.round",
5946    "Math.sign",
5947    "Math.sin",
5948    "Math.sinh",
5949    "Math.sqrt",
5950    "Math.tan",
5951    "Math.tanh",
5952    "Math.trunc",
5953    "JSON.stringify",
5954    "JSON.parse",
5955    "JSON.rawJSON",
5956    "JSON.isRawJSON",
5957    "Object.keys",
5958    "Object.values",
5959    "Object.entries",
5960    "Object.assign",
5961    "Object.freeze",
5962    "Object.is",
5963    "Object.fromEntries",
5964    "Object.getPrototypeOf",
5965    "Object.setPrototypeOf",
5966    "Object.create",
5967    "Object.getOwnPropertyNames",
5968    "Object.getOwnPropertySymbols",
5969    "Object.defineProperty",
5970    "Object.getOwnPropertyDescriptor",
5971    "Object.getOwnPropertyDescriptors",
5972    "Object.defineProperties",
5973    "Object.isFrozen",
5974    "Object.isSealed",
5975    "Object.seal",
5976    "Object.preventExtensions",
5977    "Object.isExtensible",
5978    "Object.hasOwn",
5979    "Object.groupBy",
5980    "Array.isArray",
5981    "Array.from",
5982    "Array.fromAsync",
5983    "Array.of",
5984    "Number.isFinite",
5985    "Number.isInteger",
5986    "Number.isNaN",
5987    "Number.isSafeInteger",
5988    "Number.parseFloat",
5989    "Number.parseInt",
5990    "String.fromCharCode",
5991    "String.fromCodePoint",
5992    "String.raw",
5993    "Symbol.for",
5994    "Symbol.keyFor",
5995    "BigInt.asIntN",
5996    "BigInt.asUintN",
5997    "Proxy.revocable",
5998    "Reflect.defineProperty",
5999    "Reflect.deleteProperty",
6000    "Reflect.apply",
6001    "Reflect.construct",
6002    "Reflect.get",
6003    "Reflect.getOwnPropertyDescriptor",
6004    "Reflect.getPrototypeOf",
6005    "Reflect.has",
6006    "Reflect.isExtensible",
6007    "Reflect.ownKeys",
6008    "Reflect.preventExtensions",
6009    "Reflect.set",
6010    "Reflect.setPrototypeOf",
6011    "Promise.resolve",
6012    "Promise.reject",
6013    "Promise.all",
6014    "Promise.allSettled",
6015    "Promise.race",
6016    "Promise.any",
6017    "Promise.withResolvers",
6018    "Promise.try",
6019    "RegExp.escape",
6020    "Error.isError",
6021    "Map.groupBy",
6022    "Response.json",
6023    "Response.error",
6024    "Response.redirect",
6025    "AbortSignal.abort",
6026    "AbortSignal.timeout",
6027    "process.nextTick",
6028    "Error.captureStackTrace",
6029    "require.resolve",
6030    "require.resolve.paths",
6031    "process.memoryUsage.rss",
6032];
6033
6034/// The `name` and `length` a builtin function reports, from the generated
6035/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6036/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6037/// and the core-module functions, whose arity is not specified anywhere.
6038pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6039    crate::arity::BUILTIN_ARITY
6040        .binary_search_by(|(k, _, _)| (*k).cmp(key))
6041        .ok()
6042        .map(|i| {
6043            let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6044            (name, len)
6045        })
6046}
6047
6048/// The `name` a builtin function reports. The table answers for an intrinsic;
6049/// anything else falls back to the last segment of the key, which is what the
6050/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6051/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6052/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6053pub fn builtin_name(key: &str) -> &str {
6054    if let Some((name, _)) = builtin_meta(key) {
6055        return name;
6056    }
6057    match key.strip_prefix("@proto:") {
6058        Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6059        // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6060        // with a `get` prefix), which is what `util.inspect` prints for it and
6061        // what a library reads to identify one.
6062        None => key.rsplit('.').next().unwrap_or(key),
6063    }
6064}
6065
6066/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6067/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6068/// `"get size"` it has to build.
6069pub fn proto_getter_name(key: &str) -> Option<String> {
6070    let (verb, rest) = match key.strip_prefix("@protoget:") {
6071        Some(rest) => ("get", rest),
6072        None => ("set", key.strip_prefix("@protoset:")?),
6073    };
6074    let (_, member) = rest.split_once(':')?;
6075    Some(format!("{verb} {member}"))
6076}
6077
6078pub fn is_known_builtin(name: &str) -> bool {
6079    // Binary search over a sorted INDEX of the two tables rather than a scan of
6080    // both. This runs on every call whose callee is a builtin — `call_method`
6081    // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6082    // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6083    // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6084    // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6085    // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6086    // now. The index is built once per process and derived FROM those tables, so
6087    // it cannot drift from them.
6088    //
6089    // That is an operation count, not a measured time, and NO wall-clock win is
6090    // claimed. Re-measured in isolation (this hunk alone applied to the previous
6091    // commit, interleaved against it, minimums over ten rounds each): the A/B
6092    // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6093    // the A/A control — the SAME binary under both labels — came out 1.084,
6094    // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6095    // this machine the change is not distinguishable from noise. It is kept for
6096    // the comparison count and because it cannot drift from the tables it is
6097    // derived from, not because anything got faster.
6098    static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6099    let sorted = SORTED.get_or_init(|| {
6100        let mut v: Vec<&'static str> = GLOBAL_FUNCS
6101            .iter()
6102            .chain(NS_METHODS.iter())
6103            .copied()
6104            .collect();
6105        v.sort_unstable();
6106        v
6107    });
6108    sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6109}
6110
6111// ── dynamic functions (runtime source → callable) ────────────────────────────
6112
6113/// Build a callable from a complete function-expression source text — the ONE
6114/// dynamic-function generator on this frontend.
6115///
6116/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6117/// wrapping parentheses needed to parse it as an expression: those are added
6118/// here, and `src` itself is retained so `Function.prototype.toString` reports
6119/// what V8 reports. The two callers synthesize different text and both shapes
6120/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6121///
6122/// The body runs in the MODULE scope, never the constructing function's scope
6123/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6124/// *global* environment). That also makes a `var` inside the body a function
6125/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6126/// returns 6 and leaves `globalThis.zz` `undefined`.
6127pub fn dynamic_function(src: &str) -> Result<Value, String> {
6128    let f = crate::eval_in_global_scope(&format!("({src})"))?;
6129    with_host(|h| {
6130        let s = h.new_str(src.to_string());
6131        h.set_fn_prop(&f, "@@source", s);
6132    });
6133    Ok(f)
6134}
6135
6136/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6137///
6138/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6139/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6140/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6141/// arguments at all, both the parameter list and the body are empty.
6142///
6143/// Measured on node v26.7.0:
6144///
6145/// ```text
6146/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6147/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
6148/// new Function('a,b','c','return [a,b,c]').length === 3
6149/// new Function('a','b','return a+b').name       === 'anonymous'
6150/// ```
6151pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6152    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6153    let (params, body) = match parts.split_last() {
6154        Some((body, params)) => (params.join(","), body.clone()),
6155        None => (String::new(), String::new()),
6156    };
6157    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6158}
6159
6160/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6161/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6162/// other route to the same function value is an INDIRECT eval and evaluates in
6163/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6164/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6165/// do not.
6166///
6167/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6168pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6169    let v = arg.cloned().unwrap_or(Value::Undef);
6170    let is_string =
6171        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6172    if !is_string {
6173        return Ok(v);
6174    }
6175    let src = with_host(|h| h.str_of(&v));
6176    // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6177    // decides both the early errors the COMPILE raises and the variable
6178    // environment below. An INDIRECT one is global-scope sloppy code.
6179    let caller_strict = direct && with_host(|h| h.current_strict());
6180    let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6181    if !direct {
6182        return host::run_chunk_in_global_scope(chunk);
6183    }
6184    // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6185    // so its `var`s and function declarations die with it. Only a SLOPPY one
6186    // shares the caller's, which is the form that can inject a binding — and
6187    // sharing it unconditionally meant `eval('var x=1')` inside strict code
6188    // left `x` behind.
6189    let strict = caller_strict
6190        || src.trim_start().starts_with("'use strict'")
6191        || src.trim_start().starts_with("\"use strict\"");
6192    if !strict {
6193        // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6194        // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6195        // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6196        // or `class` declared inside therefore dies with the eval; every one of
6197        // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6198        // behind and `let a=1; eval('let a=2')` overwrote it.
6199        //
6200        // `push_scope` is exactly that split: `var` and a hoisted function
6201        // declaration bind to `base_env`, which this does not touch.
6202        with_host(|h| h.push_scope());
6203        let out = host::run_chunk_on(chunk);
6204        with_host(|h| h.pop_scope());
6205        return out;
6206    }
6207    let prev = with_host(|h| h.push_var_scope());
6208    let out = host::run_chunk_on(chunk);
6209    with_host(|h| h.pop_var_scope(prev));
6210    out
6211}
6212
6213/// Call a resolved builtin function (global or `namespace.method`).
6214pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6215    // `require(spec)`: the ENTRY script's top-level require — core module first,
6216    // else the CommonJS loader resolving from the entry file's directory.
6217    if name == "require" {
6218        let spec = with_host(|h| h.str_of(&arg0(&args)));
6219        return crate::module::require(&spec, &crate::module::entry_dir());
6220    }
6221    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6222    // into the loader, resolving `spec` against the module's own directory.
6223    if name == "__cjs_require" {
6224        let spec = with_host(|h| h.str_of(&arg0(&args)));
6225        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6226        return crate::module::require(&spec, std::path::Path::new(&from));
6227    }
6228    if name == "process.memoryUsage.rss" {
6229        return Ok(crate::stdlib::process::memory_usage_rss());
6230    }
6231    if name == "require.resolve.paths" {
6232        let spec = with_host(|h| h.str_of(&arg0(&args)));
6233        // A core module is not looked up on disk at all.
6234        if crate::stdlib::is_core(&spec) {
6235            return Ok(with_host(|h| h.null()));
6236        }
6237        let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6238        return Ok(with_host(|h| {
6239            let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6240            h.new_array(items)
6241        }));
6242    }
6243    // A `require.extensions` entry. This runtime's loader does not dispatch
6244    // through the map, so calling one is the loader's own behaviour for that
6245    // extension rather than a hook point.
6246    if let Some(ext) = name.strip_prefix("@@extension:") {
6247        let _ = ext;
6248        return Ok(Value::Undef);
6249    }
6250    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6251    if name == "require.resolve" {
6252        let spec = with_host(|h| h.str_of(&arg0(&args)));
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, &crate::module::entry_dir()) {
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    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6266    // path (core modules resolve to the bare specifier, as in Node).
6267    if name == "__cjs_resolve" {
6268        let spec = with_host(|h| h.str_of(&arg0(&args)));
6269        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6270        if crate::stdlib::is_core(&spec) {
6271            return Ok(with_host(|h| h.new_str(spec)));
6272        }
6273        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6274            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6275            None => Err(crate::host::plain_coded_error(
6276                "Error",
6277                "MODULE_NOT_FOUND",
6278                &format!("Cannot find module '{spec}'"),
6279            )),
6280        };
6281    }
6282    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6283    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6284    // stack-introspection pattern used by `depd`), it is called with a synthetic
6285    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6286    if name == "Error.captureStackTrace" {
6287        let target = arg0(&args);
6288        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6289        let stack = match prep {
6290            Some(f)
6291                if matches!(
6292                    with_host(|h| h.get(&f).cloned()),
6293                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6294                ) =>
6295            {
6296                let sites = crate::module::callsite_stack(10)?;
6297                host::invoke(&f, vec![target.clone(), sites], None)?
6298            }
6299            _ => with_host(|h| h.new_str("")),
6300        };
6301        let _ = set_property(&target, "stack", stack);
6302        return Ok(Value::Undef);
6303    }
6304    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6305    if let Some(r) = crate::stdlib::call(name, &args) {
6306        return r;
6307    }
6308    match name {
6309        // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6310        // followed by one `    at <site>` line per call site. Reachable because
6311        // the read hands the hook out, and a library may call it directly to
6312        // render a stack it captured.
6313        DEFAULT_PREPARE => {
6314            let err = arg0(&args);
6315            let header = with_host(|h| {
6316                let name = host::lookup_chain(h, &err, "name")
6317                    .map(|v| h.str_of(&v))
6318                    .unwrap_or_else(|| "Error".to_string());
6319                let msg = host::lookup_chain(h, &err, "message")
6320                    .map(|v| h.str_of(&v))
6321                    .unwrap_or_default();
6322                if msg.is_empty() {
6323                    name
6324                } else {
6325                    format!("{name}: {msg}")
6326                }
6327            });
6328            let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6329            let lines = with_host(|h| match h.get(&sites) {
6330                Some(JsObj::Array(items)) => items.clone(),
6331                _ => Vec::new(),
6332            });
6333            let mut out = header;
6334            for s in lines {
6335                let rendered = host::to_string_value(&s)
6336                    .map(|v| with_host(|h| h.str_of(&v)))
6337                    .unwrap_or_default();
6338                out.push_str("\n    at ");
6339                out.push_str(&rendered);
6340            }
6341            Ok(with_host(|h| h.new_str(out)))
6342        }
6343        "console.log" | "console.info" | "console.debug" => {
6344            print_line(&args, false)?;
6345            Ok(Value::Undef)
6346        }
6347        "console.error" | "console.warn" => {
6348            print_line(&args, true)?;
6349            Ok(Value::Undef)
6350        }
6351        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6352        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6353        // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6354        "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6355        "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6356        "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6357        "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6358        "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6359        "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6360        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6361        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6362        // Reaching `eval` through this table means the eval FUNCTION VALUE was
6363        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6364        // Those are INDIRECT evals and run in the global scope. A literal
6365        // `eval(src)` is intercepted earlier, in `host::call_named`.
6366        "eval" => eval_source(args.first(), false),
6367        // `new Function(...)` and `Function(...)` are the same operation
6368        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6369        // [[Construct]]), so both route to the one generator.
6370        "Function" => function_ctor(&args),
6371        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6372        // (DEP0005). Node still supports it and still routes it to the same place
6373        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6374        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6375        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6376        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6377        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6378        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6379        // where Node warns.
6380        "Buffer" => {
6381            crate::stdlib::process::emit_deprecation_warning(
6382                "DEP0005",
6383                "Buffer() is deprecated due to security and usability issues. \
6384                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6385                 Buffer.from() methods instead.",
6386            );
6387            crate::stdlib::construct("Buffer", &args)
6388                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6389        }
6390        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6391        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6392        "Number.isNaN" => Ok(Value::Bool(
6393            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6394        )),
6395        "Number.isFinite" => Ok(Value::Bool(
6396            matches!(arg0(&args), Value::Float(f) if f.is_finite())
6397                || matches!(arg0(&args), Value::Int(_)),
6398        )),
6399        "String" => {
6400            if args.is_empty() {
6401                Ok(with_host(|h| h.new_str("")))
6402            } else {
6403                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6404                // is allowed); everything else via ToString method dispatch.
6405                host::string_ctor_value(&args[0])
6406            }
6407        }
6408        // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6409        // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6410        // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6411        // one — which is why this cannot just call `to_number_value`.
6412        "Number" => Ok(Value::Float(if args.is_empty() {
6413            0.0
6414        } else {
6415            let prim = host::to_primitive(&args[0], "number")?;
6416            match with_host(|h| h.as_bigint(&prim)) {
6417                Some(b) => host::bigint_to_f64(&b),
6418                None => host::to_number_value(&prim)?,
6419            }
6420        })),
6421        "BigInt" => bigint_ctor(&arg0(&args)),
6422        "RegExp" => regexp_ctor(&args),
6423        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6424        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6425        // Each argument is truncated to a uint16 and taken as one code UNIT, so
6426        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6427        // a surrogate PAIR of arguments composes into one character.
6428        "String.fromCharCode" => Ok(with_host(|h| {
6429            let units: Vec<u16> = args
6430                .iter()
6431                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6432                .collect();
6433            let s = crate::utf16::to_string_lossy(&units);
6434            h.new_str(s)
6435        })),
6436        // `fromCodePoint` takes whole code POINTS and rejects anything that is
6437        // not one — including a lone surrogate, which `fromCharCode` accepts.
6438        "String.fromCodePoint" => {
6439            let mut s = String::new();
6440            for a in &args {
6441                let n = with_host(|h| h.to_number(a));
6442                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6443                {
6444                    char::from_u32(n as u32)
6445                } else {
6446                    None
6447                };
6448                match cp {
6449                    Some(c) => s.push(c),
6450                    None => {
6451                        return Err(format!(
6452                            "RangeError: Invalid code point {}",
6453                            with_host(|h| h.str_of(a))
6454                        ))
6455                    }
6456                }
6457            }
6458            Ok(new_s(s))
6459        }
6460        "String.raw" => string_raw(&args),
6461        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6462        "Array" => construct_builtin("Array", args),
6463        "Array.of" => construct_array_like(host::current_static_this(), args),
6464        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6465        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6466        "Array.isArray" => {
6467            let v = arg0(&args);
6468            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6469            Ok(Value::Bool(
6470                matches!(
6471                    with_host(|h| h.get(&subject).cloned()),
6472                    Some(JsObj::Array(_))
6473                ) && !is_arguments(&subject),
6474            ))
6475        }
6476        "Array.from" => array_from(args),
6477        "Array.fromAsync" => array_from_async(args),
6478        "Object" => Ok(object_call(args)),
6479        "Object.keys" => object_keys(args, 0),
6480        "Object.values" => object_keys(args, 1),
6481        "Object.entries" => object_keys(args, 2),
6482        "Object.assign" => object_assign(args),
6483        "Object.freeze" => {
6484            let v = arg0(&args);
6485            reject_sealing_a_view(&v, "freeze")?;
6486            if seal_proxy(&v, true)? {
6487                return Ok(v);
6488            }
6489            with_host(|h| h.seal_object(&v, true));
6490            Ok(v)
6491        }
6492        "Object.seal" => {
6493            let v = arg0(&args);
6494            reject_sealing_a_view(&v, "seal")?;
6495            if seal_proxy(&v, false)? {
6496                return Ok(v);
6497            }
6498            with_host(|h| h.seal_object(&v, false));
6499            Ok(v)
6500        }
6501        "Object.preventExtensions" => {
6502            let v = arg0(&args);
6503            if crate::proxy::prevent_extensions(&v)? {
6504                return Ok(v);
6505            }
6506            with_host(|h| h.prevent_extensions(&v));
6507            Ok(v)
6508        }
6509        // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6510        // without coercion: it is not extensible, and vacuously frozen and
6511        // sealed. Reporting it extensible and unfrozen was the opposite of
6512        // every one of the three.
6513        "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6514        "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6515        "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6516        "Object.isFrozen" => integrity_level(&arg0(&args), true),
6517        "Object.isSealed" => integrity_level(&arg0(&args), false),
6518        "Object.isExtensible" => {
6519            let v = arg0(&args);
6520            match crate::proxy::is_extensible(&v)? {
6521                Some(b) => Ok(Value::Bool(b)),
6522                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6523            }
6524        }
6525        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6526        // distinct from -0.
6527        "Object.is" => {
6528            let a = arg0(&args);
6529            let b = args.get(1).cloned().unwrap_or(Value::Undef);
6530            let num = |v: &Value| match v {
6531                Value::Int(n) => Some(*n as f64),
6532                Value::Float(f) => Some(*f),
6533                _ => None,
6534            };
6535            let r = match (num(&a), num(&b)) {
6536                (Some(x), Some(y)) => {
6537                    if x.is_nan() && y.is_nan() {
6538                        true
6539                    } else if x == 0.0 && y == 0.0 {
6540                        x.is_sign_negative() == y.is_sign_negative()
6541                    } else {
6542                        x == y
6543                    }
6544                }
6545                _ => with_host(|h| h.strict_eq(&a, &b)),
6546            };
6547            Ok(Value::Bool(r))
6548        }
6549        "Object.fromEntries" => object_from_entries(args),
6550        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6551        // so the proxy form cannot share `prototype_of`'s infallible signature.
6552        // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6553        // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6554        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6555            if name == "Reflect.getPrototypeOf" {
6556                reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6557            }
6558            let v = arg0(&args);
6559            match crate::proxy::get_prototype_of(&v)? {
6560                Some(p) => Ok(p),
6561                None => Ok(prototype_of(&v)),
6562            }
6563        }
6564        "Object.setPrototypeOf" => {
6565            let obj = arg0(&args);
6566            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6567            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6568                reject_bad_prototype(&proto)?;
6569                crate::proxy::set_prototype_of(&obj, &proto)?;
6570                return Ok(obj);
6571            }
6572            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6573            // prototype type check, then — only for an actual object target —
6574            // the extensibility check. A PRIMITIVE target is returned untouched
6575            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6576            // extensibility test cannot come first.
6577            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6578                return Err(host::type_error(
6579                    "Object.setPrototypeOf called on null or undefined",
6580                ));
6581            }
6582            reject_bad_prototype(&proto)?;
6583            if with_host(|h| is_object_like(h, &obj)) {
6584                // Setting the SAME prototype is a no-op and stays legal even on a
6585                // frozen object: node v26.7.0 accepts
6586                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6587                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6588                // link still has `Object.prototype`, and comparing against the
6589                // absent link would call that a change.
6590                if would_cycle(&obj, &proto) {
6591                    return Err(host::type_error("Cyclic __proto__ value"));
6592                }
6593                if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6594                    // The receiver is named by its brand, as every other
6595                    // refusal names it — a NULL-PROTOTYPE object is
6596                    // `[object Object]`, not `#<Object>`, because it has no
6597                    // constructor to name.
6598                    return Err(host::type_error(&format!(
6599                        "{} is not extensible",
6600                        no_side_effects_string(&obj)
6601                    )));
6602                }
6603                with_host(|h| h.set_proto(&obj, proto));
6604            }
6605            Ok(obj)
6606        }
6607        "Object.create" => object_create(args),
6608        "Object.getOwnPropertyNames" => object_keys(args, 3),
6609        "Object.getOwnPropertySymbols" => {
6610            let v = arg0(&args);
6611            require_object_coercible(&v)?;
6612            let syms = proxy_or_own_symbol_keys(&v)?;
6613            Ok(with_host(|h| h.new_array(syms)))
6614        }
6615        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6616        "Object.hasOwn" => {
6617            let obj = arg0(&args);
6618            let key = args.get(1).cloned().unwrap_or(Value::Undef);
6619            object_builtin_method(&obj, "hasOwnProperty", vec![key])
6620        }
6621        "Object.defineProperty" => object_define_property(args),
6622        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6623        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6624        "Object.defineProperties" => object_define_properties(args),
6625        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6626        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6627        "Object.groupBy" => object_group_by(args),
6628        "Symbol" => Ok(with_host(|h| {
6629            let desc = args
6630                .first()
6631                .filter(|a| !matches!(a, Value::Undef))
6632                .map(|a| h.str_of(a));
6633            h.new_symbol(desc)
6634        })),
6635        "Symbol.for" => Ok(with_host(|h| {
6636            let key = h.str_of(&arg0(&args));
6637            h.symbol_for(&key)
6638        })),
6639        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6640        // description read: it answers only for symbols `Symbol.for` created.
6641        // Returning the description made every symbol look registered —
6642        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6643        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6644        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6645        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6646        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6647        "Proxy.revocable" => crate::proxy::revocable(&args),
6648        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6649        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6650        // own properties, so there is no second half to append).
6651        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6652        // non-enumerable included, strings first and then the SYMBOLS.
6653        "Reflect.ownKeys" => {
6654            let v = arg0(&args);
6655            reflect_require_object(&v, "ownKeys")?;
6656            let names = object_keys(args, 3)?;
6657            let syms = proxy_or_own_symbol_keys(&v)?;
6658            if syms.is_empty() {
6659                return Ok(names);
6660            }
6661            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6662            all.extend(syms);
6663            Ok(with_host(|h| h.new_array(all)))
6664        }
6665        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6666        // `Reflect.defineProperty` REPORTS success as a boolean where
6667        // `Object.defineProperty` throws (28.1.3). It was propagating the
6668        // throw, so the whole point of the reflective form was lost.
6669        "Reflect.defineProperty" => {
6670            reflect_require_object(&arg0(&args), "defineProperty")?;
6671            Ok(Value::Bool(object_define_property(args).is_ok()))
6672        }
6673        "Reflect.deleteProperty" => {
6674            let obj = arg0(&args);
6675            reflect_require_object(&obj, "deleteProperty")?;
6676            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6677            Ok(Value::Bool(delete_property(&obj, &k)?))
6678        }
6679        "Reflect.setPrototypeOf" => {
6680            let obj = arg0(&args);
6681            let p = args.get(1).cloned().unwrap_or(Value::Undef);
6682            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6683                crate::proxy::set_prototype_of(&obj, &p)?;
6684                return Ok(Value::Bool(true));
6685            }
6686            // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6687            // unless the new one is what it already has, which is a no-op. It
6688            // reported success and rewrote the link.
6689            // `Reflect` reports a refusal rather than throwing, for a cycle as
6690            // for a non-extensible receiver.
6691            if would_cycle(&obj, &p) {
6692                return Ok(Value::Bool(false));
6693            }
6694            if !with_host(|h| h.is_extensible(&obj)) {
6695                return Ok(Value::Bool(same_prototype(&obj, &p)));
6696            }
6697            with_host(|h| h.set_proto(&obj, p));
6698            Ok(Value::Bool(true))
6699        }
6700        "Reflect.isExtensible" => {
6701            let v = arg0(&args);
6702            match crate::proxy::is_extensible(&v)? {
6703                Some(b) => Ok(Value::Bool(b)),
6704                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6705            }
6706        }
6707        "Reflect.preventExtensions" => {
6708            let v = arg0(&args);
6709            if crate::proxy::prevent_extensions(&v)? {
6710                return Ok(Value::Bool(true));
6711            }
6712            with_host(|h| h.prevent_extensions(&v));
6713            Ok(Value::Bool(true))
6714        }
6715        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6716        "Reflect.apply" => {
6717            let f = arg0(&args);
6718            let this = args.get(1).cloned();
6719            let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6720            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6721        }
6722        // `Reflect.construct(target, args, newTarget)` — the optional third
6723        // argument decides which constructor's `prototype` the instance gets
6724        // (28.1.2). It was ignored, so the result always inherited from
6725        // `target` and `instanceof newTarget` was false.
6726        "Reflect.construct" => {
6727            let f = arg0(&args);
6728            let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6729            let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6730            host::construct_nt(&f, list, new_target)
6731        }
6732        "Reflect.has" => {
6733            let obj = arg0(&args);
6734            reflect_require_object(&obj, "has")?;
6735            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6736            Ok(Value::Bool(has_property(&obj, &k)?))
6737        }
6738        // `Reflect.get(target, key, receiver)` — the optional third argument is
6739        // what a getter sees as `this` (28.1.6). Defaults to the target.
6740        "Reflect.get" => {
6741            let obj = arg0(&args);
6742            reflect_require_object(&obj, "get")?;
6743            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6744            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6745            get_property_recv(&obj, &k, &receiver)
6746        }
6747        // `Reflect.set(target, key, value, receiver)` — the optional fourth
6748        // argument is what a setter sees as `this`, and where a DATA property
6749        // lands (28.1.13). It was ignored: the setter ran against the target
6750        // and the property was written there.
6751        "Reflect.set" => {
6752            let obj = arg0(&args);
6753            reflect_require_object(&obj, "set")?;
6754            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6755            let v = args.get(2).cloned().unwrap_or(Value::Undef);
6756            let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6757            Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6758        }
6759        "JSON.stringify" => json_stringify(args),
6760        "JSON.parse" => json_parse(args),
6761        "JSON.rawJSON" => json_raw(args),
6762        "JSON.isRawJSON" => json_is_raw(args),
6763        "structuredClone" => structured_clone(args),
6764        // The deferred drain a `Readable.from` schedules; the suffix is the
6765        // stream's heap index.
6766        _ if name.starts_with("@@transformCb:") => {
6767            let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6768            crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6769            Ok(Value::Undef)
6770        }
6771        _ if name.starts_with("@@streamFlush:") => {
6772            let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6773            crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6774            Ok(Value::Undef)
6775        }
6776        // Same implementation the `buffer` module exposes; only the binding was
6777        // missing.
6778        "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6779            .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6780        "fetch" => crate::stdlib::fetch::fetch(&args),
6781        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6782        // suffix is the signal's heap index.
6783        _ if name.starts_with("@@aborttimeout:") => {
6784            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6785            crate::stdlib::fetch::fire_timeout_abort(idx)
6786        }
6787        // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6788        // implementation. Nothing here waits on backpressure, so it only has to
6789        // BE callable — an implementation that ends with `cb()`, which the
6790        // stream contract requires, would otherwise throw.
6791        "@@streamWriteCallback" => Ok(Value::Undef),
6792        "queueMicrotask" | "process.nextTick" => {
6793            let cb = arg0(&args);
6794            require_callback(&cb)?;
6795            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6796            enqueue_microtask(name == "process.nextTick", cb, rest);
6797            Ok(Value::Undef)
6798        }
6799        "setTimeout" | "setInterval" | "setImmediate" => {
6800            require_callback(&arg0(&args))?;
6801            Ok(schedule_timer(name, args))
6802        }
6803        "clearTimeout" | "clearInterval" | "clearImmediate" => {
6804            clear_timer(&arg0(&args));
6805            Ok(Value::Undef)
6806        }
6807        "Promise.resolve" => promise_resolve(arg0(&args)),
6808        "Promise.reject" => promise_reject(arg0(&args)),
6809        "Promise.all" => promise_all(args, AllMode::All),
6810        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6811        "Promise.race" => promise_race(args, false),
6812        "Promise.any" => promise_race(args, true),
6813        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6814        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6815        "Promise.withResolvers" => promise_with_resolvers(),
6816        "Promise.try" => promise_try(args),
6817        "RegExp.escape" => regexp_escape(args),
6818        "Error.isError" => error_is_error(args),
6819        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6820        // `cb(item, i)` result (SameValueZero), each value an array of members.
6821        "Map.groupBy" => map_group_by(args),
6822        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6823        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6824        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6825        // The executor a species-constructed promise is built with: it does
6826        // nothing, because the caller settles the result through its id.
6827        "@@pnoop" => Ok(Value::Undef),
6828        _ if name.starts_with("@@presolve:") => {
6829            let id: u32 = name[11..].parse().unwrap_or(0);
6830            host::resolve_promise_val(id, arg0(&args));
6831            Ok(Value::Undef)
6832        }
6833        _ if name.starts_with("@@preject:") => {
6834            let id: u32 = name[10..].parse().unwrap_or(0);
6835            host::reject_promise_val(id, arg0(&args));
6836            Ok(Value::Undef)
6837        }
6838        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6839        // index so calling it twice is the spec's no-op rather than a re-tear.
6840        _ if name.starts_with("@@prevoke:") => {
6841            let i: u32 = name[10..].parse().unwrap_or(0);
6842            Ok(crate::proxy::revoke(i))
6843        }
6844        _ if name.starts_with("@@finpass:") => {
6845            // finally(cb) on fulfill: run cb, await whatever it returned, then
6846            // pass the original value through.
6847            let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6848            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6849            Ok(finally_chain(result, arg0(&args), false))
6850        }
6851        _ if name.starts_with("@@finthrow:") => {
6852            // finally(cb) on reject: same, then re-throw the original reason.
6853            let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6854            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6855            Ok(finally_chain(result, arg0(&args), true))
6856        }
6857        // The two thunks `finally_chain` hangs off that awaited promise. Each
6858        // carries the value it must reinstate in a one-slot cell, since a
6859        // builtin is identified only by its name and cannot close over one.
6860        _ if name.starts_with("@@finret:") => {
6861            let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6862            get_property(&Value::Obj(i), "0")
6863        }
6864        _ if name.starts_with("@@finrethrow:") => {
6865            let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6866            let reason = get_property(&Value::Obj(i), "0")?;
6867            with_host(|h| h.exc = Some(reason.clone()));
6868            Err(with_host(|h| error_string(h, &reason)))
6869        }
6870        _ => Err(host::type_error(&format!("{name} is not a function"))),
6871    }
6872}
6873
6874/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6875/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6876/// (matching Node's messages).
6877/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6878/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6879/// BigInt`. The old text said "value" literally, for every input.
6880fn bigint_convert_error(v: &Value) -> String {
6881    let shown = with_host(|h| h.str_of(v));
6882    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6883}
6884
6885/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6886/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6887/// BigInt arithmetic's operand check.
6888///
6889/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6890/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6891/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6892/// the type table — booleans convert (`true` → `1n`), strings parse with a
6893/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6894///
6895/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6896///
6897/// ```text
6898/// a[0] = true            → 1n
6899/// a[0] = '12'            → 12n
6900/// a[0] = []              → 0n        (ToPrimitive → "" → 0n)
6901/// a[0] = ['3']           → 3n
6902/// a[0] = 1               → TypeError: Cannot convert 1 to a BigInt
6903/// a[0] = new Number(3)   → TypeError: Cannot convert 3 to a BigInt
6904/// a[0] = 'a'             → SyntaxError: Cannot convert a to a BigInt
6905/// a[0] = {}              → SyntaxError: Cannot convert [object Object] to a BigInt
6906/// ```
6907pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6908    let prim = host::to_primitive(v, "number")?;
6909    if let Some(b) = with_host(|h| match h.get(&prim) {
6910        Some(JsObj::BigInt(b)) => Some(b.clone()),
6911        _ => None,
6912    }) {
6913        return Ok(b);
6914    }
6915    match &prim {
6916        Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6917        Value::Str(s) => host::parse_bigint_str(s)
6918            .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6919        _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6920            let s = with_host(|h| h.str_of(&prim));
6921            host::parse_bigint_str(&s)
6922                .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6923        }
6924        _ => Err(bigint_convert_error(&prim)),
6925    }
6926}
6927
6928fn bigint_ctor(v: &Value) -> Result<Value, String> {
6929    use num_bigint::BigInt;
6930    let big = match v {
6931        Value::Bool(b) => BigInt::from(*b as i64),
6932        Value::Int(n) => BigInt::from(*n),
6933        Value::Float(f) => {
6934            if !f.is_finite() || f.fract() != 0.0 {
6935                let disp = with_host(|h| h.str_of(v));
6936                return Err(format!(
6937                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6938                ));
6939            }
6940            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6941            // .toString` switches to exponential notation at 1e21, and
6942            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6943            // threw `Cannot convert value to a BigInt` where node returns
6944            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6945            // value, which is also what node reports for a magnitude past the
6946            // exactly-representable range (`BigInt(1e30)` is
6947            // `1000000000000000019884624838656n` in both).
6948            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6949                Some(b) => b,
6950                None => return Err(bigint_convert_error(v)),
6951            }
6952        }
6953        Value::Str(s) => match host::parse_bigint_str(s) {
6954            Some(b) => b,
6955            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6956        },
6957        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6958            Some(JsObj::BigInt(b)) => b,
6959            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6960                Some(b) => b,
6961                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6962            },
6963            _ => return Err(bigint_convert_error(v)),
6964        },
6965        _ => return Err(bigint_convert_error(v)),
6966    };
6967    Ok(with_host(|h| h.new_bigint(big)))
6968}
6969
6970/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6971/// its source (and flags, unless new ones are given).
6972fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6973    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6974        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6975        _ => {
6976            let a0 = arg0(args);
6977            // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6978            // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6979            // pattern instead of throwing.
6980            let src = if matches!(a0, Value::Undef) {
6981                String::new()
6982            } else {
6983                arg_to_string(args, 0)?
6984            };
6985            (src, None)
6986        }
6987    };
6988    let flags = match args.get(1) {
6989        Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6990        _ => existing_flags.unwrap_or_default(),
6991    };
6992    // An empty source compiles as the JS canonical `(?:)`.
6993    let src = if source.is_empty() {
6994        "(?:)".to_string()
6995    } else {
6996        source
6997    };
6998    crate::regexp::build_regexp(&src, &flags)
6999}
7000
7001/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
7002/// two's-complement (signed) or unsigned integer.
7003fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
7004    use num_bigint::BigInt;
7005    use num_traits::Signed;
7006    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
7007    if bits < 0 {
7008        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7009    }
7010    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7011        Some(b) => b,
7012        None => return Err(host::type_error("Cannot convert to a BigInt")),
7013    };
7014    let bits = bits as u32;
7015    if bits == 0 {
7016        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7017    }
7018    let modulus = BigInt::from(1) << bits; // 2^bits
7019                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7020    let mut r = &x % &modulus;
7021    if r.is_negative() {
7022        r += &modulus;
7023    }
7024    if !unsigned {
7025        let half = BigInt::from(1) << (bits - 1);
7026        if r >= half {
7027            r -= &modulus;
7028        }
7029    }
7030    Ok(with_host(|h| h.new_bigint(r)))
7031}
7032
7033/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7034/// interleaved with the substitutions.
7035fn string_raw(args: &[Value]) -> Result<Value, String> {
7036    let call_site = arg0(args);
7037    let raw = get_property(&call_site, "raw")?;
7038    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7039    let mut out = String::new();
7040    for (i, r) in raws.iter().enumerate() {
7041        out.push_str(&with_host(|h| h.str_of(r)));
7042        if i + 1 < raws.len() {
7043            if let Some(sub) = args.get(i + 1) {
7044                out.push_str(&with_host(|h| h.str_of(sub)));
7045            }
7046        }
7047    }
7048    Ok(with_host(|h| h.new_str(out)))
7049}
7050
7051/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7052/// fresh object; objects pass through.
7053/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7054/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7055/// ordinary object that also has internal slots, so it can carry own properties
7056/// like anything else — but its heap variant holds only those slots, so a write
7057/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7058pub fn uses_side_table(v: &Value) -> bool {
7059    matches!(
7060        with_host(|h| h.kind_of(v)),
7061        Some(
7062            ObjKind::Map
7063                | ObjKind::Set
7064                | ObjKind::Promise
7065                | ObjKind::RegExp
7066                | ObjKind::Generator
7067                | ObjKind::Symbol
7068                | ObjKind::BigInt
7069                | ObjKind::Iter
7070        )
7071    )
7072}
7073
7074fn object_call(args: Vec<Value>) -> Value {
7075    let a = arg0(&args);
7076    // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7077    // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7078    if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7079        return with_host(|h| h.new_object(IndexMap::new()));
7080    }
7081    to_object(&a)
7082}
7083
7084/// The name of the wrapper a primitive boxes into, or `None` when the value is
7085/// already an object.
7086fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7087    match v {
7088        Value::Int(_) | Value::Float(_) => Some("Number"),
7089        Value::Bool(_) => Some("Boolean"),
7090        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7091            Some(JsObj::Str(_)) => Some("String"),
7092            Some(JsObj::Symbol { .. }) => Some("Symbol"),
7093            Some(JsObj::BigInt(_)) => Some("BigInt"),
7094            _ => None,
7095        },
7096        _ => None,
7097    }
7098}
7099
7100/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7101/// for every other value. The slot is a hidden `@@primitive` own property —
7102/// the same `@@` marker convention the engine already uses for internal state,
7103/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7104pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7105    with_host(|h| match h.get(v) {
7106        Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7107        _ => None,
7108    })
7109}
7110
7111/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7112/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7113///
7114/// A String wrapper also owns its index properties and `length`, which is what
7115/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7116/// non-writable and non-configurable, as the exotic `String` object's are.
7117pub fn to_object(v: &Value) -> Value {
7118    let Some(ctor) = wrapper_ctor_of(v) else {
7119        return v.clone();
7120    };
7121    with_host(|h| h.ensure_wrapper_protos());
7122    let chars: Vec<String> = if ctor == "String" {
7123        with_host(|h| h.str_of(v))
7124            .chars()
7125            .map(|c| c.to_string())
7126            .collect()
7127    } else {
7128        Vec::new()
7129    };
7130    with_host(|h| {
7131        let mut m: IndexMap<String, Value> = IndexMap::new();
7132        for (i, c) in chars.iter().enumerate() {
7133            let s = h.new_str(c.clone());
7134            m.insert(i.to_string(), s);
7135        }
7136        let w = h.new_object(m);
7137        if ctor == "String" {
7138            for i in 0..chars.len() {
7139                h.set_prop_attrs(
7140                    &w,
7141                    &i.to_string(),
7142                    host::PropAttrs {
7143                        writable: false,
7144                        enumerable: true,
7145                        configurable: false,
7146                    },
7147                );
7148            }
7149            let len = Value::Float(chars.len() as f64);
7150            if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7151                p.insert("length".into(), len);
7152            }
7153            h.set_prop_attrs(
7154                &w,
7155                "length",
7156                host::PropAttrs {
7157                    writable: false,
7158                    enumerable: false,
7159                    configurable: false,
7160                },
7161            );
7162        }
7163        if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7164            p.insert("@@primitive".into(), v.clone());
7165        }
7166        if let Some(proto) = h.native_proto(ctor) {
7167            h.set_proto(&w, proto);
7168        }
7169        w
7170    })
7171}
7172
7173/// Construct via `new` for the builtin constructors.
7174pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7175    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7176    if let Some(r) = crate::stdlib::construct(name, &args) {
7177        return r;
7178    }
7179    match name {
7180        "Array" => {
7181            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7182            // A single NUMBER argument is a length and is validated as one
7183            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7184            // `new Array(2**32)` are all `RangeError: Invalid array length` on
7185            // node v26.7.0; only a non-number single argument is an element.
7186            if args.len() == 1 {
7187                if let Value::Float(_) | Value::Int(_) = args[0] {
7188                    let n = host::to_array_length(&args[0])?;
7189                    // Every element of `new Array(n)` is a HOLE, not a stored
7190                    // `undefined`: `Object.keys(Array(3))` is `[]`.
7191                    return Ok(with_host(|h| {
7192                        let a = h.new_array(vec![Value::Undef; n]);
7193                        h.mark_hole_range(&a, 0..n);
7194                        a
7195                    }));
7196                }
7197            }
7198            Ok(with_host(|h| h.new_array(args)))
7199        }
7200        "Object" => Ok(object_call(args)),
7201        // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7202        // form. These were not constructors at all, so every one threw.
7203        "String" => Ok(to_object(&host::to_string_value(
7204            &args
7205                .first()
7206                .cloned()
7207                .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7208        )?)),
7209        "Number" => Ok(to_object(&Value::Float(match args.first() {
7210            Some(a) => host::to_number_value(a)?,
7211            None => 0.0,
7212        }))),
7213        "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7214            h.truthy(&arg0(&args))
7215        })))),
7216        "Map" | "WeakMap" => {
7217            let weak = name == "WeakMap";
7218            let m = with_host(|h| {
7219                h.alloc(JsObj::Map {
7220                    entries: indexmap::IndexMap::new(),
7221                    weak,
7222                })
7223            });
7224            if let Some(init) = args
7225                .first()
7226                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7227            {
7228                // Stepped, not drained: an entry that is not a pair has to
7229                // stop the construction at that element and CLOSE the iterator
7230                // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7231                // infinite source was never reached and the constructor HUNG.
7232                host::iter_for_each(init, |p, _| {
7233                    // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7234                    // is iterable, so without this check `new Map(["ab"])`
7235                    // happily stored `'a' => 'b'` instead of throwing — and over
7236                    // an infinite source it never stopped.
7237                    if !with_host(|h| is_object_like(h, &p)) {
7238                        let shown = with_host(|h| h.str_of(&p));
7239                        return Err(host::type_error(&format!(
7240                            "Iterator value {shown} is not an entry object"
7241                        )));
7242                    }
7243                    // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7244                    // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7245                    // gives `undefined => undefined`, and an array-LIKE entry
7246                    // works. Iterating it instead accepted a string as a pair
7247                    // and rejected the array-like.
7248                    let k = get_property(&p, "0")?;
7249                    let v = get_property(&p, "1")?;
7250                    map_method(&m, "set", vec![k, v])?;
7251                    Ok(())
7252                })?;
7253            }
7254            Ok(m)
7255        }
7256        "Set" | "WeakSet" => {
7257            let weak = name == "WeakSet";
7258            let s = with_host(|h| {
7259                h.alloc(JsObj::Set {
7260                    entries: indexmap::IndexMap::new(),
7261                    weak,
7262                })
7263            });
7264            if let Some(init) = args
7265                .first()
7266                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7267            {
7268                host::iter_for_each(init, |v, _| {
7269                    set_method(&s, "add", vec![v])?;
7270                    Ok(())
7271                })?;
7272            }
7273            Ok(s)
7274        }
7275        "Promise" => new_promise(arg0(&args)),
7276        "Proxy" => crate::proxy::create(&args),
7277        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7278        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7279        // deprecation wrapper this way, so `require('body-parser')` — and with it
7280        // `require('express')` — dies at load without it.
7281        "Function" => function_ctor(&args),
7282        "RegExp" => regexp_ctor(&args),
7283        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7284        "Error" => make_error_checked(name, &args),
7285        // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7286        // legacy numeric `code` follows from it.
7287        "DOMException" => Ok(dom_exception(&args)),
7288        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7289        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7290    }
7291}
7292
7293/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7294/// outside the table — including the default `"Error"` — reports 0.
7295pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7296    ("IndexSizeError", 1.0),
7297    ("DOMStringSizeError", 2.0),
7298    ("HierarchyRequestError", 3.0),
7299    ("WrongDocumentError", 4.0),
7300    ("InvalidCharacterError", 5.0),
7301    ("NoDataAllowedError", 6.0),
7302    ("NoModificationAllowedError", 7.0),
7303    ("NotFoundError", 8.0),
7304    ("NotSupportedError", 9.0),
7305    ("InUseAttributeError", 10.0),
7306    ("InvalidStateError", 11.0),
7307    ("SyntaxError", 12.0),
7308    ("InvalidModificationError", 13.0),
7309    ("NamespaceError", 14.0),
7310    ("InvalidAccessError", 15.0),
7311    ("ValidationError", 16.0),
7312    ("TypeMismatchError", 17.0),
7313    ("SecurityError", 18.0),
7314    ("NetworkError", 19.0),
7315    ("AbortError", 20.0),
7316    ("URLMismatchError", 21.0),
7317    ("QuotaExceededError", 22.0),
7318    ("TimeoutError", 23.0),
7319    ("InvalidNodeTypeError", 24.0),
7320    ("DataCloneError", 25.0),
7321];
7322
7323/// The static name a `DOMException` code is exposed under: the error name minus
7324/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7325/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7326fn legacy_code_name(error_name: &str) -> String {
7327    let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7328    let mut out = String::new();
7329    for (i, c) in stem.chars().enumerate() {
7330        if c.is_ascii_uppercase() && i > 0 {
7331            out.push('_');
7332        }
7333        out.push(c.to_ascii_uppercase());
7334    }
7335    out.push_str("_ERR");
7336    out
7337}
7338
7339/// `new DOMException(message, name)`.
7340///
7341/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7342/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7343/// legacy number that name maps to.
7344pub fn dom_exception(args: &[Value]) -> Value {
7345    let message = match args.first() {
7346        None | Some(Value::Undef) => String::new(),
7347        Some(v) => with_host(|h| h.str_of(v)),
7348    };
7349    let name = match args.get(1) {
7350        None | Some(Value::Undef) => "Error".to_string(),
7351        Some(v) => with_host(|h| h.str_of(v)),
7352    };
7353    with_host(|h| dom_exception_with(h, &name, &message))
7354}
7355
7356/// `dom_exception` for a caller that already holds the host borrow.
7357pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7358    let name = name.to_string();
7359    let message = message.to_string();
7360    let code = DOM_EXCEPTION_CODES
7361        .iter()
7362        .find(|(n, _)| *n == name)
7363        .map(|(_, c)| *c)
7364        .unwrap_or(0.0);
7365    let head = if message.is_empty() {
7366        name.clone()
7367    } else {
7368        format!("{name}: {message}")
7369    };
7370    let e = synth_error(h, &head);
7371    {
7372        let nv = h.new_str(name);
7373        let mv = h.new_str(message);
7374        let sv = h.new_str(head);
7375        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7376            // `name`, `message` and `code` are PROTOTYPE accessors over internal
7377            // slots in node, so `stack` is the instance's only own property.
7378            // Storing them as own keys would show up in
7379            // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7380            p.shift_remove("message");
7381            p.insert("@@domName".into(), nv);
7382            p.insert("@@domMessage".into(), mv);
7383            p.insert("@@domCode".into(), Value::Float(code));
7384            p.insert("stack".into(), sv);
7385        }
7386        h.ensure_error_protos();
7387        if let Some(proto) = host::error_proto_of(h, "DOMException") {
7388            h.set_proto(&e, proto);
7389        }
7390    }
7391    e
7392}
7393
7394/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7395/// rather than as own properties. `None` for anything else.
7396pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7397    let slot = match name {
7398        "name" => "@@domName",
7399        "message" => "@@domMessage",
7400        "code" => "@@domCode",
7401        _ => return None,
7402    };
7403    with_host(|h| match h.get(recv) {
7404        Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7405        _ => None,
7406    })
7407}
7408
7409/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7410/// throw a value with extra own properties on it.
7411pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7412    let m = with_host(|h| h.new_str(msg.to_string()));
7413    make_error_inner(name, &[m])
7414}
7415
7416/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7417/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7418/// rendered `Symbol(desc)` into `.message`.
7419fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7420    if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7421        // AggregateError's message is its SECOND argument.
7422        let idx = usize::from(name == "AggregateError");
7423        if idx == 0 {
7424            host::to_string_value(m)?;
7425        } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7426            host::to_string_value(m2)?;
7427        }
7428    }
7429    Ok(make_error_inner(name, args))
7430}
7431
7432fn make_error_inner(name: &str, args: &[Value]) -> Value {
7433    // `new AggregateError(errors, message)` takes the causes FIRST; every other
7434    // error constructor takes the message first.
7435    let agg = name == "AggregateError";
7436    let (errors, args) = if agg {
7437        (
7438            Some(args.first().cloned().unwrap_or(Value::Undef)),
7439            args.get(1..).unwrap_or(&[]),
7440        )
7441    } else {
7442        (None, args)
7443    };
7444    with_host(|h| {
7445        h.ensure_error_protos();
7446        let mut props: IndexMap<String, Value> = IndexMap::new();
7447        let msg = args
7448            .first()
7449            .filter(|a| !matches!(a, Value::Undef))
7450            .map(|a| h.str_of(a));
7451        if let Some(m) = &msg {
7452            let mv = h.new_str(m.clone());
7453            props.insert("message".into(), mv);
7454        }
7455        // `.stack` is engine-specific; a simple `Name: message` header line
7456        // suffices for parity (the fuzzer never prints raw stacks).
7457        //
7458        // V8 formats that header LAZILY, on the first read, from whatever `name`
7459        // and `message` the error carries at that moment — which is why the
7460        // near-universal
7461        //
7462        //     class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7463        //
7464        // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7465        // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7466        // carries the frames so the read can redo it; see `materialize_stack`.
7467        let frames = h.stack_frames();
7468        let stack = match &msg {
7469            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7470            _ => format!("{name}{frames}"),
7471        };
7472        let sv = h.new_str(stack);
7473        props.insert("stack".into(), sv);
7474        let raw = h.new_str(frames);
7475        props.insert("@@stackRaw".into(), raw);
7476        if let Some(errs) = errors {
7477            // Materialize the iterable into the own `errors` array property.
7478            let items = h.iter_vec(&errs).unwrap_or_default();
7479            let arr = h.new_array(items);
7480            props.insert("errors".into(), arr);
7481        }
7482        // `new Error(msg, { cause })` (ES2022): installed only when the options
7483        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7484        let opts = args.get(1);
7485        if let Some(cause) = opts.and_then(|o| match h.get(o) {
7486            Some(JsObj::Object(p)) => p.get("cause").cloned(),
7487            _ => None,
7488        }) {
7489            props.insert("cause".into(), cause);
7490        }
7491        let e = h.new_object(props);
7492        if let Some(p) = host::error_proto_of(h, name) {
7493            h.set_proto(&e, p);
7494        }
7495        // Every own slot an error constructor installs is non-enumerable in V8,
7496        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7497        // `{}` — properties a *script* later assigns stay enumerable.
7498        for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7499            h.hide_prop(&e, k);
7500        }
7501        e
7502    })
7503}
7504
7505fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7506    // Node's console.log(...args) === util.format(...args): printf-style
7507    // substitution when the first arg is a format string, else inspect-and-join.
7508    // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7509    // the caller instead of printing a line — so nothing is written on failure.
7510    let line: String = crate::stdlib::util::format(args)?;
7511    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7512    Ok(())
7513}
7514
7515fn arg0(args: &[Value]) -> Value {
7516    args.first().cloned().unwrap_or(Value::Undef)
7517}
7518/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7519/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7520/// instead rendered `Symbol(desc)` into its result and reported nothing.
7521fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7522    let v = args.get(i).cloned().unwrap_or(Value::Undef);
7523    let sv = host::to_string_value(&v)?;
7524    Ok(with_host(|h| h.str_of(&sv)))
7525}
7526
7527fn arg_num(args: &[Value], i: usize) -> f64 {
7528    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7529}
7530
7531fn is_integer(v: Value) -> bool {
7532    match v {
7533        Value::Int(_) => true,
7534        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7535        _ => false,
7536    }
7537}
7538fn is_safe_integer(v: Value) -> bool {
7539    match v {
7540        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7541        Value::Int(_) => true,
7542        _ => false,
7543    }
7544}
7545
7546/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7547/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7548/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7549fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7550    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7551    const UNRESERVED: &[u8] =
7552        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7553    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7554    const RESERVED: &[u8] = b";,/?:@&=+$#";
7555    let mut out = String::with_capacity(s.len());
7556    for &b in s.as_bytes() {
7557        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7558            out.push(b as char);
7559        } else {
7560            out.push('%');
7561            out.push(
7562                char::from_digit((b >> 4) as u32, 16)
7563                    .unwrap()
7564                    .to_ascii_uppercase(),
7565            );
7566            out.push(
7567                char::from_digit((b & 0xf) as u32, 16)
7568                    .unwrap()
7569                    .to_ascii_uppercase(),
7570            );
7571        }
7572    }
7573    Ok(with_host(|h| h.new_str(out)))
7574}
7575
7576/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7577/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7578/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7579fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7580    const RESERVED: &[u8] = b";,/?:@&=+$#";
7581    let bytes = s.as_bytes();
7582    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7583    let mut i = 0;
7584    while i < bytes.len() {
7585        if bytes[i] == b'%' {
7586            if i + 2 >= bytes.len() {
7587                return Err("URIError: URI malformed".into());
7588            }
7589            let hi = (bytes[i + 1] as char).to_digit(16);
7590            let lo = (bytes[i + 2] as char).to_digit(16);
7591            match (hi, lo) {
7592                (Some(h), Some(l)) => {
7593                    let byte = (h * 16 + l) as u8;
7594                    // decodeURI keeps reserved-delimiter escapes literal.
7595                    if uri && RESERVED.contains(&byte) {
7596                        out.extend_from_slice(&bytes[i..i + 3]);
7597                    } else {
7598                        out.push(byte);
7599                    }
7600                    i += 3;
7601                }
7602                _ => return Err("URIError: URI malformed".into()),
7603            }
7604        } else {
7605            out.push(bytes[i]);
7606            i += 1;
7607        }
7608    }
7609    match String::from_utf8(out) {
7610        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7611        Err(_) => Err("URIError: URI malformed".into()),
7612    }
7613}
7614
7615/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7616/// present in every engine and still reached by old libraries (jQuery's cookie
7617/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7618/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7619/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7620/// character yields the two escapes of its surrogate pair
7621/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7622///
7623/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7624/// it keeps `@*_+-./` and drops `!~'()`.
7625fn legacy_escape(s: &str) -> Result<Value, String> {
7626    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7627    let mut out = String::with_capacity(s.len());
7628    for u in s.encode_utf16() {
7629        if u < 0x100 {
7630            if KEEP.contains(&(u as u8)) {
7631                out.push(u as u8 as char);
7632            } else {
7633                out.push_str(&format!("%{u:02X}"));
7634            }
7635        } else {
7636            out.push_str(&format!("%u{u:04X}"));
7637        }
7638    }
7639    Ok(with_host(|h| h.new_str(out)))
7640}
7641
7642/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7643/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7644/// `%XX` or `%uXXXX` escape is passed through literally
7645/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7646///
7647/// Decoding is done in code-unit space and re-joined at the end so a
7648/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7649fn legacy_unescape(s: &str) -> Result<Value, String> {
7650    let b = s.as_bytes();
7651    let hex = |i: usize, n: usize| -> Option<u16> {
7652        if i + n > b.len() {
7653            return None;
7654        }
7655        let mut v: u16 = 0;
7656        for &c in &b[i..i + n] {
7657            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7658        }
7659        Some(v)
7660    };
7661    let units: Vec<u16> = s.encode_utf16().collect();
7662    let mut out: Vec<u16> = Vec::with_capacity(units.len());
7663    let mut i = 0;
7664    while i < b.len() {
7665        // Escapes are pure ASCII, so a byte index is a unit index up to here —
7666        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7667        if b[i] == b'%' {
7668            if let Some(u) = hex(i + 1, 2) {
7669                out.push(u);
7670                i += 3;
7671                continue;
7672            }
7673            if b.get(i + 1) == Some(&b'u') {
7674                if let Some(u) = hex(i + 2, 4) {
7675                    out.push(u);
7676                    i += 6;
7677                    continue;
7678                }
7679            }
7680        }
7681        let c = s[i..].chars().next().unwrap_or('%');
7682        let mut buf = [0u16; 2];
7683        out.extend_from_slice(c.encode_utf16(&mut buf));
7684        i += c.len_utf8();
7685    }
7686    Ok(with_host(|h| {
7687        h.new_str(crate::utf16::to_string_lossy(&out))
7688    }))
7689}
7690
7691/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7692/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7693/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7694/// instead of converting it, which both swallowed that throw and ignored any
7695/// `toString` the value defines.
7696fn parse_int(args: &[Value]) -> Result<f64, String> {
7697    // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7698    let sv = host::to_string_value(&arg0(args))?;
7699    // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7700    // infallible read below does no `ToPrimitive`, so an object radix came out
7701    // as NaN and the parse silently fell back to auto-detection.
7702    let radix = match args.get(1) {
7703        Some(r) if !matches!(r, Value::Undef) => {
7704            vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7705        }
7706        _ => args.to_vec(),
7707    };
7708    Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7709}
7710
7711fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7712    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7713    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7714    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7715    let radix_arg = args
7716        .get(1)
7717        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7718    let radix = match radix_arg {
7719        Some(0) | None => None,
7720        Some(r) if (2..=36).contains(&r) => Some(r as u32),
7721        Some(_) => return f64::NAN,
7722    };
7723    let t = crate::utf16::js_trim_start(s);
7724    let (neg, digits) = match t.strip_prefix('-') {
7725        Some(rest) => (true, rest),
7726        None => (false, t.strip_prefix('+').unwrap_or(t)),
7727    };
7728    let (radix, digits) = match radix {
7729        Some(16) => (
7730            16u32,
7731            digits
7732                .strip_prefix("0x")
7733                .or_else(|| digits.strip_prefix("0X"))
7734                .unwrap_or(digits),
7735        ),
7736        Some(r) => (r, digits),
7737        None => {
7738            if let Some(hex) = digits
7739                .strip_prefix("0x")
7740                .or_else(|| digits.strip_prefix("0X"))
7741            {
7742                (16, hex)
7743            } else {
7744                (10, digits)
7745            }
7746        }
7747    };
7748    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7749    if valid.is_empty() {
7750        return f64::NAN;
7751    }
7752    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7753    // digits and the error was mapped to `NaN`, so
7754    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7755    // asks for the mathematical value rounded to a Number, which is what
7756    // repeated multiply-accumulate in `f64` produces.
7757    let n = if radix == 10 {
7758        // Rust's decimal float parser is correctly rounded; digit-by-digit
7759        // multiply-accumulate is not, and drifted a ULP on long inputs
7760        // (`parseInt("999999999999999999999999")` came out
7761        // 1.0000000000000003e+24 rather than 1e+24).
7762        valid.parse::<f64>().unwrap_or(f64::NAN)
7763    } else {
7764        let mut n = 0.0f64;
7765        for c in valid.chars() {
7766            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7767        }
7768        n
7769    };
7770    if neg {
7771        -n
7772    } else {
7773        n
7774    }
7775}
7776
7777/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7778fn parse_float(args: &[Value]) -> Result<f64, String> {
7779    let sv = host::to_string_value(&arg0(args))?;
7780    Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7781}
7782
7783fn parse_float_str(s: &str) -> f64 {
7784    let t = crate::utf16::js_trim_start(s);
7785    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7786    let inf_body = t
7787        .strip_prefix('+')
7788        .or_else(|| t.strip_prefix('-'))
7789        .unwrap_or(t);
7790    if inf_body.starts_with("Infinity") {
7791        return if t.starts_with('-') {
7792            f64::NEG_INFINITY
7793        } else {
7794            f64::INFINITY
7795        };
7796    }
7797    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7798    // not the same as the longest run of characters that could appear in one:
7799    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7800    // only valid once a digit follows `e`. Tracking `end` at every character
7801    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7802    // came back NaN.
7803    let mut end = 0;
7804    let bytes = t.as_bytes();
7805    let mut seen_dot = false;
7806    let mut seen_e = false;
7807    let mut digits_before_dot = false;
7808    for (i, &c) in bytes.iter().enumerate() {
7809        match c {
7810            b'0'..=b'9' => {
7811                if !seen_dot && !seen_e {
7812                    digits_before_dot = true;
7813                }
7814                end = i + 1;
7815            }
7816            // A sign is only meaningful leading, or straight after the exponent
7817            // marker; it never completes a literal on its own.
7818            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7819            // `1.` is a complete literal; a bare `.` is not.
7820            b'.' if !seen_dot && !seen_e => {
7821                seen_dot = true;
7822                if digits_before_dot {
7823                    end = i + 1;
7824                }
7825            }
7826            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7827            _ => break,
7828        }
7829    }
7830    if end == 0 {
7831        return f64::NAN;
7832    }
7833    t[..end].parse::<f64>().unwrap_or(f64::NAN)
7834}
7835
7836/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7837/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7838/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7839/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7840/// rather than 1.
7841pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7842    if exp == 0.0 {
7843        return 1.0;
7844    }
7845    if base.is_nan() || exp.is_nan() {
7846        return f64::NAN;
7847    }
7848    if base.abs() == 1.0 && exp.is_infinite() {
7849        return f64::NAN;
7850    }
7851    base.powf(exp)
7852}
7853
7854fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7855    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7856    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7857    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7858    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7859    // exception: it never reads an argument, so `Math.random(1n)` is fine.
7860    // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7861    // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7862    // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7863    // the host itself and cannot run inside another borrow.
7864    let is_bigint = |a: &Value| {
7865        if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7866            return true;
7867        }
7868        match wrapped_primitive(a) {
7869            Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7870            None => false,
7871        }
7872    };
7873    if fname != "random" && args.iter().any(is_bigint) {
7874        return Err(host::type_error(
7875            "Cannot convert a BigInt value to a number",
7876        ));
7877    }
7878    // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7879    // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7880    // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7881    // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7882    // variadic, and coercing only the first few silently DROPPED the rest —
7883    // `Math.max(...gen)` over five values answered for four of them.
7884    let mut coerced = Vec::with_capacity(args.len());
7885    for a in args {
7886        if matches!(a, Value::Undef) {
7887            coerced.push(a.clone());
7888            continue;
7889        }
7890        let p = host::to_primitive(a, "number")?;
7891        coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7892    }
7893    let args: &[Value] = &coerced;
7894    let x = arg_num(args, 0);
7895    let r = match fname {
7896        "floor" => x.floor(),
7897        "ceil" => x.ceil(),
7898        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7899        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7900        // before the floor sees it, so it answers 1 for the largest double below
7901        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7902        // perturbs integers above 2^52, where `x + 0.5` is no longer
7903        // representable (`Math.round(4503599627370497)` must be the input).
7904        // Splitting the zero-band cases out first also carries the signed zero
7905        // the spec asks for without a post-hoc patch.
7906        "round" => {
7907            if !x.is_finite() || x == 0.0 {
7908                x
7909            } else if x > 0.0 && x < 0.5 {
7910                0.0
7911            } else if (-0.5..0.0).contains(&x) {
7912                -0.0
7913            } else {
7914                // |x| >= 0.5, so `floor` and the subtraction are both exact
7915                // (every double >= 2^52 is already an integer and yields 0 here).
7916                let f = x.floor();
7917                if x - f >= 0.5 {
7918                    f + 1.0
7919                } else {
7920                    f
7921                }
7922            }
7923        }
7924        "trunc" => x.trunc(),
7925        "abs" => x.abs(),
7926        "sign" => {
7927            if x.is_nan() {
7928                f64::NAN
7929            } else if x > 0.0 {
7930                1.0
7931            } else if x < 0.0 {
7932                -1.0
7933            } else {
7934                x
7935            }
7936        }
7937        "sqrt" => x.sqrt(),
7938        "cbrt" => x.cbrt(),
7939        "exp" => x.exp(),
7940        "log" => x.ln(),
7941        "log2" => x.log2(),
7942        "log10" => x.log10(),
7943        "sin" => x.sin(),
7944        "cos" => x.cos(),
7945        "tan" => x.tan(),
7946        "asin" => x.asin(),
7947        "acos" => x.acos(),
7948        "atan" => x.atan(),
7949        "atan2" => x.atan2(arg_num(args, 1)),
7950        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7951        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7952        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7953        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7954        // clause is shared.
7955        "pow" => js_pow(x, arg_num(args, 1)),
7956        // Hyperbolics and the two precision-preserving log/exp forms.
7957        "sinh" => x.sinh(),
7958        "cosh" => x.cosh(),
7959        "tanh" => x.tanh(),
7960        "asinh" => x.asinh(),
7961        "acosh" => x.acosh(),
7962        "atanh" => x.atanh(),
7963        "log1p" => x.ln_1p(),
7964        "expm1" => x.exp_m1(),
7965        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7966        // wraparound, reinterpret as a signed 32-bit result.
7967        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7968        "hypot" => {
7969            // Scale by the largest magnitude before squaring — this avoids the
7970            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7971            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7972            let mut max = 0.0f64;
7973            for x in &xs {
7974                if x.abs() > max {
7975                    max = x.abs();
7976                }
7977            }
7978            if xs.iter().any(|x| x.is_infinite()) {
7979                f64::INFINITY
7980            } else if max == 0.0 || !max.is_finite() {
7981                max
7982            } else {
7983                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7984                max * s.sqrt()
7985            }
7986        }
7987        "random" => pseudo_random(),
7988        "max" => {
7989            if args.is_empty() {
7990                f64::NEG_INFINITY
7991            } else {
7992                let mut m = f64::NEG_INFINITY;
7993                for a in args {
7994                    let n = with_host(|h| h.to_number(a));
7995                    if n.is_nan() {
7996                        return Ok(Value::Float(f64::NAN));
7997                    }
7998                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7999                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
8000                    // must not keep the -0 the first iteration installed.
8001                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
8002                        m = n;
8003                    }
8004                }
8005                m
8006            }
8007        }
8008        "min" => {
8009            if args.is_empty() {
8010                f64::INFINITY
8011            } else {
8012                let mut m = f64::INFINITY;
8013                for a in args {
8014                    let n = with_host(|h| h.to_number(a));
8015                    if n.is_nan() {
8016                        return Ok(Value::Float(f64::NAN));
8017                    }
8018                    // Mirror of `max`: -0 ranks below +0 even though `<` says
8019                    // they are equal, so `Math.min(0, -0)` is -0.
8020                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8021                        m = n;
8022                    }
8023                }
8024                m
8025            }
8026        }
8027        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8028        "clz32" => {
8029            let u = if x.is_finite() {
8030                x.trunc().rem_euclid(4294967296.0) as u32
8031            } else {
8032                0
8033            };
8034            u.leading_zeros() as f64
8035        }
8036        // Round to the nearest single-precision float.
8037        "fround" => (x as f32) as f64,
8038        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8039    };
8040    Ok(Value::Float(r))
8041}
8042
8043/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8044/// Node by nature; kept simple).
8045fn pseudo_random() -> f64 {
8046    use std::cell::Cell;
8047    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8048    SEED.with(|s| {
8049        let mut x = s.get();
8050        x ^= x << 13;
8051        x ^= x >> 7;
8052        x ^= x << 17;
8053        s.set(x);
8054        (x >> 11) as f64 / (1u64 << 53) as f64
8055    })
8056}
8057
8058// ── Object.* ──────────────────────────────────────────────────────────────────
8059
8060/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8061/// properties (10.4.3 `StringExoticObject`).
8062///
8063/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8064/// an exotic object whose own keys are its code-unit indices plus `length`;
8065/// this is the descriptor half of that. (The KEY half lives in
8066/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8067/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8068///
8069/// A boxed `String` object is deliberately NOT routed here: it can carry
8070/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8071/// existing path already reports them alongside the indices.
8072fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8073    // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8074    // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8075    // never catches one.
8076    let s = match v {
8077        Value::Str(s) => (**s).clone(),
8078        _ => with_host(|h| match h.get(v) {
8079            Some(JsObj::Str(s)) => Some(s.clone()),
8080            _ => None,
8081        })?,
8082    };
8083    let units = crate::utf16::Units::of(&s);
8084    Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8085}
8086
8087fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8088    let v = arg0(&args);
8089    require_object_coercible(&v)?;
8090    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8091    // reports every own STRING key the trap named; the enumerating modes
8092    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8093    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8094        if mode == 3 {
8095            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8096            return Ok(with_host(|h| {
8097                let out: Vec<Value> = keys
8098                    .into_iter()
8099                    .filter(|k| !host::is_symbol_key(k))
8100                    .map(|k| h.new_str(k))
8101                    .collect();
8102                h.new_array(out)
8103            }));
8104        }
8105        // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8106        // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8107        // Going through `own_enum_entries` fired the `get` trap once per key, so
8108        // the observable trap sequence carried a trailing `get` node does not
8109        // emit, and a trap with side effects ran when it should not have.
8110        if mode == 0 {
8111            let keys = crate::proxy::own_enum_string_keys(&v)?;
8112            return Ok(with_host(|h| {
8113                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8114                h.new_array(out)
8115            }));
8116        }
8117        let entries = crate::proxy::own_enum_entries(&v)?;
8118        return Ok(with_host(|h| {
8119            let out: Vec<Value> = entries
8120                .into_iter()
8121                .map(|(k, val)| match mode {
8122                    0 => h.new_str(k),
8123                    1 => val,
8124                    _ => {
8125                        let ks = h.new_str(k);
8126                        h.new_array(vec![ks, val])
8127                    }
8128                })
8129                .collect();
8130            h.new_array(out)
8131        }));
8132    }
8133    // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8134    // .prototype`, `String.prototype`, the error hierarchy — answers from the
8135    // generated table too. It was answering from its own property map instead,
8136    // which carries neither the right names nor V8's order: `Symbol.prototype`
8137    // reported `toLocaleString` and omitted `description`, and
8138    // `String.prototype` omitted `length` and every Annex B HTML method.
8139    //
8140    // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8141    // the two representations of a prototype cannot answer differently.
8142    let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8143        .filter(|ns| intrinsic_proto_members(ns).is_some());
8144    // A builtin prototype namespace that exposes enumerable methods for copying
8145    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8146    if let Some(ns) = real_proto_ns.or_else(|| {
8147        with_host(|h| match h.get(&v) {
8148            Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8149            _ => None,
8150        })
8151    }) {
8152        // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8153        // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8154        // interface, which the table records per name.
8155        if let Some(members) = intrinsic_proto_members(&ns) {
8156            let ctor = ns.trim_end_matches(".prototype");
8157            let mut names: Vec<String> = members
8158                .iter()
8159                .filter(|m| mode == 3 || m.starts_with('+'))
8160                .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8161                // `getOwnPropertyNames` reports STRING keys only; the table's
8162                // `@@` entries are symbol-keyed members and belong to
8163                // `getOwnPropertySymbols` instead.
8164                .filter(|m| !m.starts_with("@@"))
8165                .collect();
8166            // Plus whatever a script patched onto this prototype under a NEW
8167            // name — an ordinary enumerable own property, so it lists in every
8168            // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8169            // an assignment that `Array.prototype.patch` read back happily.
8170            //
8171            // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8172            // that member's attributes alone: restoring a saved `join` must not
8173            // turn it into an enumerable key.
8174            for k in with_host(|h| h.builtin_static_keys(&ns)) {
8175                if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8176                    names.push(k);
8177                }
8178            }
8179            return Ok(with_host(|h| {
8180                let out: Vec<Value> = names
8181                    .iter()
8182                    .map(|name| {
8183                        // An accessor member has no thunk — `Map.prototype.size`
8184                        // is not a function — so a VALUE read of one answers
8185                        // undefined rather than synthesizing a callable.
8186                        let val = |h: &mut host::JsHost| {
8187                            if let Some(v) = h.builtin_static(&ns, name) {
8188                                return v;
8189                            }
8190                            let key = format!("@proto:{ctor}:{name}");
8191                            if builtin_meta(&key).is_some() {
8192                                h.alloc(JsObj::Builtin(key))
8193                            } else {
8194                                Value::Undef
8195                            }
8196                        };
8197                        match mode {
8198                            1 => val(h),
8199                            2 => {
8200                                let ks = h.new_str(name.clone());
8201                                let v = val(h);
8202                                h.new_array(vec![ks, v])
8203                            }
8204                            _ => h.new_str(name.clone()),
8205                        }
8206                    })
8207                    .collect();
8208                h.new_array(out)
8209            }));
8210        }
8211        if let Some(names) = builtin_proto_method_names(&ns) {
8212            return Ok(with_host(|h| {
8213                let out: Vec<Value> = names
8214                    .iter()
8215                    .map(|name| match mode {
8216                        1 => h.alloc(JsObj::Builtin(format!(
8217                            "@proto:{}:{name}",
8218                            ns.trim_end_matches(".prototype")
8219                        ))),
8220                        2 => {
8221                            let ks = h.new_str(*name);
8222                            let val = h.alloc(JsObj::Builtin(format!(
8223                                "@proto:{}:{name}",
8224                                ns.trim_end_matches(".prototype")
8225                            )));
8226                            h.new_array(vec![ks, val])
8227                        }
8228                        _ => h.new_str(*name),
8229                    })
8230                    .collect();
8231                h.new_array(out)
8232            }));
8233        }
8234        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8235        // keys are the members node-js implements, each resolved to the same
8236        // first-class value a property read would give.
8237        let mut names = crate::stdlib::namespace_keys(&ns);
8238        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8239        // its members live in the builtin dispatch table. They are
8240        // non-enumerable in V8, so they surface only under
8241        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8242        if names.is_empty() && mode == 3 {
8243            let prefix = format!("{ns}.");
8244            // A builtin constructor's own `length`/`name`/`prototype` come
8245            // first, as they do in V8.
8246            if is_builtin_ctor(&ns) {
8247                names.extend(["length", "name", "prototype"].map(str::to_string));
8248            }
8249            names.extend(
8250                NS_METHODS
8251                    .iter()
8252                    .filter_map(|q| q.strip_prefix(&prefix))
8253                    .map(|m| m.to_string()),
8254            );
8255            // The numeric constants are members too. Without them
8256            // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8257            // actually answers — the eight it dropped being `PI` and its
8258            // siblings, which read fine and now own a descriptor as well.
8259            names.extend(
8260                namespace_constants(&ns)
8261                    .iter()
8262                    .map(|(k, _)| (*k).to_string()),
8263            );
8264        }
8265        // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8266        // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8267        // was `[]`, which said the function had no properties at all while both
8268        // of them read back a value. `length` is listed only where the intrinsic
8269        // table has an arity, so the names never advertise a read that answers
8270        // `undefined`.
8271        if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8272            if builtin_meta(&ns).is_some() {
8273                names.push("length".to_string());
8274            }
8275            names.push("name".to_string());
8276        }
8277        // Whatever a script assigned onto the namespace, in assignment order and
8278        // after the built-in members — an ordinary enumerable own property, so
8279        // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8280        // missing from every listing, which made a patched prototype read as
8281        // unpatched to any code that enumerates rather than reads.
8282        for k in with_host(|h| h.builtin_static_keys(&ns)) {
8283            if !names.contains(&k) {
8284                names.push(k);
8285            }
8286        }
8287        if !names.is_empty() {
8288            let entries: Vec<(String, Value)> = names
8289                .into_iter()
8290                .map(|k| {
8291                    let val = namespace_property(&ns, &k);
8292                    (k, val)
8293                })
8294                .collect();
8295            return Ok(with_host(|h| {
8296                let out: Vec<Value> = entries
8297                    .into_iter()
8298                    .map(|(k, val)| match mode {
8299                        1 => val,
8300                        2 => {
8301                            let ks = h.new_str(k);
8302                            h.new_array(vec![ks, val])
8303                        }
8304                        _ => h.new_str(k),
8305                    })
8306                    .collect();
8307                h.new_array(out)
8308            }));
8309        }
8310    }
8311    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8312    // non-enumerable ones, plus the exotic `length` an array carries.
8313    let entries: Vec<(String, Value)> = with_host(|h| {
8314        if mode == 3 {
8315            // An array's exotic `length` is already placed (after the indices,
8316            // before the ordinary string keys) by `own_key_names`.
8317            return h
8318                .own_key_names(&v, false)
8319                .into_iter()
8320                .map(|k| (k, Value::Undef))
8321                .collect();
8322        }
8323        Vec::new()
8324    });
8325    // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8326    // key/value pairs, so asking it for them ran every enumerable getter —
8327    // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8328    // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8329    // side effects:
8330    //
8331    //     let n = 0; const o = { get g() { n++; return 1 } };
8332    //     Object.keys(o); n   // was 1, node says 0
8333    //
8334    // `values`/`entries` (modes 1 and 2) do read, and still do.
8335    let entries = match mode {
8336        3 => entries,
8337        0 => with_host(|h| h.own_enum_key_names(&v))
8338            .into_iter()
8339            .map(|k| (k, Value::Undef))
8340            .collect(),
8341        _ => host::own_enum_entries_deep(&v)?,
8342    };
8343    Ok(with_host(|h| {
8344        let out: Vec<Value> = entries
8345            .into_iter()
8346            .map(|(k, val)| match mode {
8347                0 | 3 => h.new_str(k),
8348                1 => val,
8349                _ => {
8350                    let ks = h.new_str(k);
8351                    h.new_array(vec![ks, val])
8352                }
8353            })
8354            .collect();
8355        h.new_array(out)
8356    }))
8357}
8358
8359fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8360    let target = arg0(&args);
8361    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8362    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8363    require_object_coercible(&target)?;
8364    for src in args.iter().skip(1) {
8365        // `Object.assign` copies own *enumerable* properties, running any getter
8366        // — symbol-keyed ones included (7.3.25).
8367        let entries = host::own_enum_entries_deep(src)?;
8368        let syms = with_host(|h| h.own_symbol_entries(src));
8369        // A plain object target is filled in place (one borrow, then a single
8370        // re-canonicalization of the integer-index keys).
8371        let filled = with_host(|h| {
8372            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8373                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8374                    p.insert(k, v);
8375                }
8376                host::canonicalize_own_keys(p);
8377                return true;
8378            }
8379            false
8380        });
8381        // Any OTHER target — an array being the common one — goes through the
8382        // ordinary Set path. The in-place branch above matched `JsObj::Object`
8383        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8384        // returned the untouched array: no error, just a missing property. The
8385        // Set path is what an `arr.extra = 9` assignment already used, so index
8386        // and non-index keys land where they do for a direct write.
8387        if !filled {
8388            for (k, v) in entries.into_iter().chain(syms) {
8389                set_property(&target, &k, v)?;
8390            }
8391        }
8392    }
8393    Ok(target)
8394}
8395
8396fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8397    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8398    let mut props: IndexMap<String, Value> = IndexMap::new();
8399    for p in pairs {
8400        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8401        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8402        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8403        props.insert(key, val);
8404    }
8405    Ok(with_host(|h| h.new_object(props)))
8406}
8407
8408/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8409/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8410/// members mapped to that key, in first-seen key order.
8411fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8412    group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8413    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8414    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8415    // Stepped, not drained: the callback runs per element, so a throwing one
8416    // stops at the first. Draining first meant an infinite source never reached
8417    // the callback at all and the call HUNG.
8418    host::iter_for_each(&arg0(&args), |item, i| {
8419        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8420        let key = with_host(|h| h.property_key(&key_v));
8421        groups.entry(key).or_default().push(item);
8422        Ok(())
8423    })?;
8424    let props: IndexMap<String, Value> = with_host(|h| {
8425        groups
8426            .into_iter()
8427            .map(|(k, v)| (k, h.new_array(v)))
8428            .collect()
8429    });
8430    let obj = with_host(|h| h.new_object(props));
8431    // A null-prototype object (as Node returns), so it has no inherited members.
8432    with_host(|h| {
8433        let nv = h.null();
8434        h.set_proto(&obj, nv);
8435    });
8436    Ok(obj)
8437}
8438
8439/// The `groupBy` family words a non-iterable argument its OWN way — a third
8440/// vocabulary, alongside the array-literal spread's and the call spread's:
8441///
8442/// ```text
8443/// null / undefined   "<Name> called on null or undefined"
8444/// anything else      "<typeof> [value ]is not iterable (cannot read property
8445///                     Symbol(Symbol.iterator))"
8446/// ```
8447///
8448/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8449/// string and a boolean name the value too.
8450fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8451    if with_host(|h| h.is_nullish(v)) {
8452        return Err(host::type_error(&format!(
8453            "{name} called on null or undefined"
8454        )));
8455    }
8456    // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8457    // here, so the stepping loop below then saw an exhausted one — the finite
8458    // case returned an empty group and the infinite case was back to hanging.
8459    let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8460    if with_host(|h| host::is_callable(h, &iter_fn)) {
8461        return Ok(());
8462    }
8463    Err(host::type_error(&not_iterable_typed(v)))
8464}
8465
8466/// The `<type> <value> is not iterable (cannot read property
8467/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8468/// no name to report: a plain object, a symbol and a bigint name only their
8469/// TYPE; a number, a string and a boolean name the value too.
8470pub(crate) fn not_iterable_typed(v: &Value) -> String {
8471    let shown = with_host(|h| {
8472        let kind = h.type_of(v);
8473        match kind {
8474            "object" | "symbol" | "bigint" => kind.to_string(),
8475            "string" => format!("string \"{}\"", h.str_of(v)),
8476            _ => format!("{kind} {}", h.str_of(v)),
8477        }
8478    });
8479    format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8480}
8481
8482/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8483/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8484fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8485    group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8486    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8487    let m = with_host(|h| {
8488        h.alloc(JsObj::Map {
8489            entries: IndexMap::new(),
8490            weak: false,
8491        })
8492    });
8493    // Stepped for the same reason `Object.groupBy` is.
8494    host::iter_for_each(&arg0(&args), |item, i| {
8495        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8496        let existing = map_method(&m, "get", vec![key_v.clone()])?;
8497        if matches!(existing, Value::Undef) {
8498            let arr = with_host(|h| h.new_array(vec![item]));
8499            map_method(&m, "set", vec![key_v, arr])?;
8500        } else {
8501            with_host(|h| {
8502                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8503                    a.push(item);
8504                }
8505            });
8506        }
8507        Ok(())
8508    })?;
8509    Ok(m)
8510}
8511
8512/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8513/// element and each `mapFn` result.
8514///
8515/// Written in JavaScript and compiled once, because the operation IS an async
8516/// function: a Rust builtin runs outside any coroutine and has no way to await,
8517/// so draining a promise from there would mean running the microtask queue by
8518/// hand. Delegating to the engine's own `async`/`for await` keeps the
8519/// suspension semantics — and the ordering they imply — exactly the language's.
8520///
8521/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8522/// array-like. Everything iterable goes through `for await`, which awaits a sync
8523/// source's elements individually — that is what makes
8524/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8525/// accepted because an async generator object does not expose
8526/// `Symbol.asyncIterator` on this frontend.
8527fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8528    thread_local! {
8529        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8530    }
8531    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8532        const out = []; let i = 0;\n\
8533        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8534        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8535            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8536        if (iterable) {\n\
8537            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8538            return out;\n\
8539        }\n\
8540        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8541        while (i < len) { await step(items[i]); }\n\
8542        return out;\n\
8543    })";
8544    let f = IMPL.with(|c| c.borrow().clone());
8545    let f = match f {
8546        Some(f) => f,
8547        None => {
8548            let f = crate::eval_in_global_scope(SRC)?;
8549            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8550            f
8551        }
8552    };
8553    host::invoke(&f, args, None)
8554}
8555
8556fn array_from(args: Vec<Value>) -> Result<Value, String> {
8557    // `Array.from` accepts generators and user iterables, plus array-likes with a
8558    // numeric `.length`.
8559    let src = arg0(&args);
8560    if let Some(cb) = args.get(1).cloned() {
8561        // Stepped, not drained: the mapper runs per element as the iterator
8562        // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8563        // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8564        // all and HUNG, and a throwing mapper could not close the iterator.
8565        let this = this_arg(&args, 2);
8566        let mut out = Vec::new();
8567        let mapped = host::iter_for_each(&src, |v, i| {
8568            out.push(host::invoke(
8569                &cb,
8570                vec![v, Value::Float(i as f64)],
8571                this.clone(),
8572            )?);
8573            Ok(())
8574        });
8575        match mapped {
8576            Ok(()) => {}
8577            // An array-LIKE has no iterator; fall back to its indexed items.
8578            Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8579                out.clear();
8580                for (i, it) in array_like_items(&src).into_iter().enumerate() {
8581                    out.push(host::invoke(
8582                        &cb,
8583                        vec![it, Value::Float(i as f64)],
8584                        this.clone(),
8585                    )?);
8586                }
8587            }
8588            Err(e) => return Err(e),
8589        }
8590        return construct_array_like(host::current_static_this(), out);
8591    }
8592    let items = match host::iter_all(&src) {
8593        Ok(v) => v,
8594        Err(_) => array_like_items(&src),
8595    };
8596    // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8597    // result is an instance of it. It always allocated a plain array, which is
8598    // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8599    // never started.
8600    construct_array_like(host::current_static_this(), items)
8601}
8602
8603/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8604pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8605    // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8606    // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8607    // because the infallible read does no `ToPrimitive`. A throw from it is
8608    // swallowed here for the same reason the `length` read is: this helper has
8609    // no way to report one, and every caller treats an unreadable length as 0.
8610    let len = get_property(src, "length")
8611        .ok()
8612        .and_then(|l| host::to_primitive(&l, "number").ok())
8613        .map(|l| with_host(|h| h.to_number(&l)))
8614        .unwrap_or(0.0);
8615    if !len.is_finite() || len <= 0.0 {
8616        return Vec::new();
8617    }
8618    (0..len as usize)
8619        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8620        .collect()
8621}
8622
8623// ── JSON ──────────────────────────────────────────────────────────────────────
8624
8625fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8626    // A CALLABLE second argument is the replacer function, and it is checked
8627    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8628    // callable never also reaches the key-filter path below.
8629    let replacer = args
8630        .get(1)
8631        .filter(|r| with_host(|h| host::is_callable(h, r)))
8632        .cloned();
8633    // `toJSON` and the replacer run BEFORE serialization and are user code, so
8634    // the tree is rewritten first — outside the host borrow `json_str` holds,
8635    // and before the BigInt walk, which has no cycle guard of its own.
8636    //
8637    // The top-level value is a property of a synthetic wrapper `{ "": value }`
8638    // under key `""`, which is exactly the holder the replacer receives as
8639    // `this` on its first call.
8640    let root = arg0(&args);
8641    let wrapper = with_host(|h| {
8642        let mut m: IndexMap<String, Value> = IndexMap::new();
8643        m.insert(String::new(), root.clone());
8644        h.new_object(m)
8645    });
8646    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8647    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8648    // bigint form), matching Node's exact message.
8649    if with_host(|h| json_has_bigint(h, &v)) {
8650        return Err(host::type_error("Do not know how to serialize a BigInt"));
8651    }
8652    let indent = match args.get(2) {
8653        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8654        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8655        None => String::new(),
8656    };
8657    // A replacer array (args[1]) restricts which object keys are serialized.
8658    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8659        with_host(|h| match h.get(r) {
8660            Some(JsObj::Array(items)) => {
8661                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8662            }
8663            _ => None,
8664        })
8665    });
8666    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8667    match s {
8668        Some(s) => Ok(with_host(|h| h.new_str(s))),
8669        None => Ok(Value::Undef),
8670    }
8671}
8672
8673/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8674/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8675/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8676/// object survives. Applies to user methods, class methods, and the native
8677/// `Date`/`Buffer`/`URL` accessors alike.
8678///
8679/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8680/// objects currently being walked so a cyclic structure is reported rather than
8681/// spinning forever.
8682///
8683/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8684/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8685/// because the inner method is a plain (unserializable) function property of the
8686/// returned object, not a second conversion hook.
8687fn apply_to_json(
8688    holder: &Value,
8689    key: &str,
8690    v: &Value,
8691    path: &mut JsonPath,
8692    rep: Option<&Value>,
8693) -> Result<Value, String> {
8694    let mut v = v.clone();
8695    if matches!(v, Value::Obj(_)) {
8696        let tag = crate::stdlib::native_tag(&v);
8697        // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8698        // supplies one through its `get` trap. `lookup_chain` walks the
8699        // property map and never asks the handler, so a proxy carrying a
8700        // `toJSON` was serialized as a plain object instead of by its own
8701        // method — and node's trap log starts with that `get`.
8702        let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8703            get_property(&v, "toJSON")?
8704        } else {
8705            with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8706        };
8707        let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8708            || tag
8709                .as_deref()
8710                .map(crate::stdlib::has_to_json)
8711                .unwrap_or(false);
8712        if has_to_json {
8713            let k = with_host(|h| h.new_str(key.to_string()));
8714            v = host::call_method(&v, "toJSON", vec![k])?;
8715        }
8716    }
8717    if let Some(rep) = rep {
8718        let k = with_host(|h| h.new_str(key.to_string()));
8719        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8720    }
8721    // How `v` was reached from its holder, as V8 names the step in a
8722    // circular-structure message: `index 1` under an array, else `property 'k'`.
8723    let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8724        format!("index {key}")
8725    } else {
8726        format!("property '{key}'")
8727    };
8728    json_walk_children(&v, path, &via, rep)
8729}
8730
8731/// The objects `JSON.stringify` is inside of, outermost first, each with the
8732/// step that reached it from its holder (`property 'x'` / `index 1`).
8733type JsonPath = Vec<(String, Value)>;
8734
8735/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8736/// starts at to the key that closes it. At most the first two and the last one
8737/// intermediate step are listed, with `|     ...` standing for the rest.
8738fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8739    const PREFIX: usize = 2;
8740    const POSTFIX: usize = 1;
8741    let ctor = |v: &Value| -> String {
8742        with_host(|h| match h.get(v) {
8743            Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8744            _ => match h.ctor_name(v) {
8745                n if n.is_empty() => "Object".to_string(),
8746                n => n,
8747            },
8748        })
8749    };
8750    let line = |i: usize| {
8751        format!(
8752            "\n    |     {} -> object with constructor '{}'",
8753            path[i].0,
8754            ctor(&path[i].1)
8755        )
8756    };
8757    let mut msg = format!(
8758        "Converting circular structure to JSON\n    --> starting at object with constructor '{}'",
8759        ctor(&path[start].1)
8760    );
8761    let prefix_end = path.len().min(start + 1 + PREFIX);
8762    for i in start + 1..prefix_end {
8763        msg.push_str(&line(i));
8764    }
8765    if path.len() > prefix_end + POSTFIX {
8766        msg.push_str("\n    |     ...");
8767    }
8768    for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8769        msg.push_str(&line(i));
8770    }
8771    msg.push_str(&format!("\n    --- {closing} closes the circle"));
8772    msg
8773}
8774
8775/// Whether a raw property key of a host object is one `json_str` serializes. The
8776/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8777/// invisible to JSON, so the replacer must not be invoked for them either.
8778fn json_visible_key(k: &str) -> bool {
8779    !k.starts_with("@@") && !k.starts_with('#')
8780}
8781
8782/// Recurse into the elements/properties of an already-converted value, running
8783/// `apply_to_json` for each with this value as the holder.
8784fn json_walk_children(
8785    v: &Value,
8786    path: &mut JsonPath,
8787    via: &str,
8788    rep: Option<&Value>,
8789) -> Result<Value, String> {
8790    if !matches!(v, Value::Obj(_)) {
8791        return Ok(v.clone());
8792    }
8793    // A value that contains itself has no JSON form.
8794    if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8795        return Err(host::type_error(&circular_json_message(path, start, via)));
8796    }
8797    // A Proxy owns no property map, so it is snapshotted through its traps into
8798    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8799    // — which read every member through `[[Get]]`, exactly as the snapshot does.
8800    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8801        let snap = crate::proxy::json_snapshot(v)?;
8802        path.push((via.to_string(), v.clone()));
8803        let out = json_walk_children(&snap, path, via, rep);
8804        path.pop();
8805        return out;
8806    }
8807    let obj = with_host(|h| h.get(v).cloned());
8808    path.push((via.to_string(), v.clone()));
8809    let out = (|| match obj {
8810        Some(JsObj::Array(items)) => {
8811            // Read the elements through the accessor-aware funnel: an index
8812            // with a getter must be SERIALIZED as what the getter returns, and
8813            // the backing vector still holds the stale slot.
8814            let mut resolved = items;
8815            // An index with a getter must be SERIALIZED as what the getter
8816            // returns, and it also forces a rebuild below: keeping the original
8817            // array would hand the serializer back the stale backing vector.
8818            let had_accessor = resolve_index_accessors(v, &mut resolved);
8819            let items = resolved;
8820            let mut out = Vec::with_capacity(items.len());
8821            let mut changed = had_accessor;
8822            for (i, it) in items.iter().enumerate() {
8823                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8824                changed |= !with_host(|h| h.strict_eq(&nv, it));
8825                out.push(nv);
8826            }
8827            // Keep identity when nothing changed, so an enclosing object is not
8828            // needlessly rebuilt (which would drop its property attributes).
8829            if changed {
8830                Ok(with_host(|h| h.new_array(out)))
8831            } else {
8832                Ok(v.clone())
8833            }
8834        }
8835        Some(JsObj::Object(props)) => {
8836            // An enumerable own accessor must have its getter RUN and the result
8837            // serialized. That cannot happen inside `json_str` (which holds the
8838            // host borrow), so materialize here — the same reason `toJSON` is
8839            // applied in this pass.
8840            let has_accessor = with_host(|h| {
8841                h.own_accessor_keys(v)
8842                    .iter()
8843                    .any(|k| h.prop_attrs(v, k).enumerable)
8844            });
8845            if has_accessor {
8846                let mut next: IndexMap<String, Value> = IndexMap::new();
8847                for (k, val) in host::own_enum_entries_deep(v)? {
8848                    let nv = if json_visible_key(&k) {
8849                        apply_to_json(v, &k, &val, path, rep)?
8850                    } else {
8851                        val
8852                    };
8853                    next.insert(k, nv);
8854                }
8855                return Ok(with_host(|h| h.new_object(next)));
8856            }
8857            // Only rebuild when a descendant actually changed, so plain data keeps
8858            // its identity (and its prototype / native tag).
8859            let mut next: IndexMap<String, Value> = IndexMap::new();
8860            let mut changed = false;
8861            for (k, val) in &props {
8862                let nv = if json_visible_key(k) {
8863                    apply_to_json(v, k, val, path, rep)?
8864                } else {
8865                    val.clone()
8866                };
8867                changed |= !with_host(|h| h.strict_eq(&nv, val));
8868                next.insert(k.clone(), nv);
8869            }
8870            if changed {
8871                Ok(with_host(|h| {
8872                    let o = h.new_object(next);
8873                    h.copy_prop_attrs(v, &o);
8874                    o
8875                }))
8876            } else {
8877                Ok(v.clone())
8878            }
8879        }
8880        _ => Ok(v.clone()),
8881    })();
8882    path.pop();
8883    out
8884}
8885
8886/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8887/// try to serialize (a value in an array/object) — such a value throws.
8888fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8889    match h.get(v) {
8890        Some(JsObj::BigInt(_)) => true,
8891        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8892        Some(JsObj::Object(props)) => props
8893            .iter()
8894            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8895            .any(|(_, val)| json_has_bigint(h, val)),
8896        _ => false,
8897    }
8898}
8899
8900fn json_str(
8901    h: &host::JsHost,
8902    v: &Value,
8903    indent: &str,
8904    depth: usize,
8905    keys: Option<&[String]>,
8906) -> Option<String> {
8907    let sep = if indent.is_empty() { ":" } else { ": " };
8908    match v {
8909        Value::Undef => None,
8910        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8911        Value::Int(n) => Some(n.to_string()),
8912        Value::Float(f) => Some(if f.is_finite() {
8913            host::fmt_number(*f)
8914        } else {
8915            "null".into()
8916        }),
8917        Value::Str(s) => Some(json_quote(s)),
8918        Value::Obj(_) => match h.get(v) {
8919            Some(JsObj::Str(s)) => Some(json_quote(s)),
8920            Some(JsObj::Null) => Some("null".into()),
8921            // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8922            // whole point of it, and it is why a number wider than a `double`
8923            // can survive a round trip.
8924            _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8925                Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8926                _ => None,
8927            },
8928            // A Map/Set has no ENTRIES to serialize (they are internal slots),
8929            // but any own property a script attached is serialized like an
8930            // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8931            // is `{"a":1}`.
8932            Some(JsObj::Map { .. })
8933            | Some(JsObj::Set { .. })
8934            | Some(JsObj::RegExp(_))
8935            // A Promise and a generator are ORDINARY objects to the serializer:
8936            // their state is internal slots, so they contribute no entries and
8937            // render as `{}`. They were being omitted entirely instead, so a
8938            // promise in an array became `null` and one in an object vanished.
8939            | Some(JsObj::Promise { .. })
8940            | Some(JsObj::Generator { .. }) => {
8941                let parts: Vec<String> = h
8942                    .own_enum_entries(v)
8943                    .into_iter()
8944                    .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8945                    .filter_map(|(k, val)| {
8946                        json_str(h, &val, indent, depth + 1, keys)
8947                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8948                    })
8949                    .collect();
8950                Some(wrap(&parts, "{", "}", indent, depth))
8951            }
8952            // A NON-callable builtin is a namespace object, not a function, so
8953            // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8954            // are all non-enumerable), where omitting it made the whole property
8955            // disappear from its holder.
8956            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8957                let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8958                    .into_iter()
8959                    .filter_map(|k| {
8960                        let val = h.builtin_static(n, &k)?;
8961                        json_str(h, &val, indent, depth + 1, keys)
8962                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8963                    })
8964                    .collect();
8965                Some(wrap(&parts, "{", "}", indent, depth))
8966            }
8967            // Functions and symbols are omitted (undefined) as values.
8968            Some(JsObj::Func(_))
8969            | Some(JsObj::Builtin(_))
8970            | Some(JsObj::BoundMethod { .. })
8971            | Some(JsObj::BoundFunc { .. })
8972            | Some(JsObj::Class(_))
8973            | Some(JsObj::Symbol { .. }) => None,
8974            Some(JsObj::Array(items)) => {
8975                if items.is_empty() {
8976                    return Some("[]".into());
8977                }
8978                let parts: Vec<String> = items
8979                    .iter()
8980                    .map(|x| {
8981                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8982                    })
8983                    .collect();
8984                Some(wrap(&parts, "[", "]", indent, depth))
8985            }
8986            Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8987                // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8988                // the primitive it boxes, not as the object holding it —
8989                // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8990                json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8991            }
8992            Some(JsObj::Object(props)) => {
8993                // A replacer array restricts (and orders) which keys are emitted.
8994                let parts: Vec<String> = match keys {
8995                    Some(allow) => allow
8996                        .iter()
8997                        .filter_map(|k| {
8998                            props.get(k).and_then(|val| {
8999                                json_str(h, val, indent, depth + 1, keys)
9000                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9001                            })
9002                        })
9003                        .collect(),
9004                    None => h
9005                        .own_enum_entries(v)
9006                        .iter()
9007                        .filter_map(|(k, val)| {
9008                            json_str(h, val, indent, depth + 1, keys)
9009                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9010                        })
9011                        .collect(),
9012                };
9013                if parts.is_empty() {
9014                    return Some("{}".into());
9015                }
9016                Some(wrap(&parts, "{", "}", indent, depth))
9017            }
9018            _ => Some("null".into()),
9019        },
9020        _ => Some("null".into()),
9021    }
9022}
9023
9024fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9025    if indent.is_empty() {
9026        format!("{open}{}{close}", parts.join(","))
9027    } else {
9028        let pad = indent.repeat(depth + 1);
9029        let pad_close = indent.repeat(depth);
9030        format!(
9031            "{open}\n{pad}{}\n{pad_close}{close}",
9032            parts.join(&format!(",\n{pad}"))
9033        )
9034    }
9035}
9036
9037fn json_quote(s: &str) -> String {
9038    let mut out = String::from("\"");
9039    for c in s.chars() {
9040        match c {
9041            '"' => out.push_str("\\\""),
9042            '\\' => out.push_str("\\\\"),
9043            '\n' => out.push_str("\\n"),
9044            '\t' => out.push_str("\\t"),
9045            '\r' => out.push_str("\\r"),
9046            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9047            // Backspace and form feed were missing, so they fell through to the
9048            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9049            // `""` where node produces `"\b"`. Both parse back to the same
9050            // string, so the difference is invisible to a round trip and shows
9051            // up only as a byte mismatch against a fixture or a checksum.
9052            '\u{8}' => out.push_str("\\b"),
9053            '\u{c}' => out.push_str("\\f"),
9054            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9055            _ => out.push(c),
9056        }
9057    }
9058    out.push('"');
9059    out
9060}
9061
9062fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9063    let s = with_host(|h| h.str_of(&arg0(&args)));
9064    let mut p = JsonParser {
9065        chars: s.chars().collect(),
9066        pos: 0,
9067        prims: Vec::new(),
9068        record: args
9069            .get(1)
9070            .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9071    };
9072    p.skip_ws();
9073    if p.peek().is_none() {
9074        return Err("SyntaxError: Unexpected end of JSON input".into());
9075    }
9076    let v = p.parse_value()?;
9077    let value_end = p.pos;
9078    p.skip_ws();
9079    // Anything after the top-level value is an error — the parser used to accept
9080    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9081    if let Some(c) = p.peek() {
9082        // V8 names the token kind only when it butts directly against the value
9083        // (`01` -> "Unexpected number at position 1"); with whitespace between
9084        // it is just a non-whitespace character (`1 2`).
9085        // Only a digit butted directly against a completed number literal —
9086        // V8's number scanner is still in number context there. `5"x"` and
9087        // `[0,1]0` exit the scanner cleanly and get the generic message.
9088        let after_number = value_end > 0
9089            && p.pos == value_end
9090            && p.chars[value_end - 1].is_ascii_digit()
9091            && c.is_ascii_digit();
9092        return Err(if after_number {
9093            p.err_at("Unexpected number", p.pos)
9094        } else {
9095            p.err_trailing(p.pos)
9096        });
9097    }
9098    // Optional reviver: walk bottom-up, transforming each (key, value).
9099    if let Some(reviver) = args
9100        .get(1)
9101        .filter(|r| with_host(|h| host::is_callable(h, r)))
9102        .cloned()
9103    {
9104        // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9105        // constructs before the walk.
9106        let root = with_host(|h| {
9107            let mut m: IndexMap<String, Value> = IndexMap::new();
9108            m.insert(String::new(), v.clone());
9109            h.new_object(m)
9110        });
9111        return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9112    }
9113    Ok(v)
9114}
9115
9116/// `JSON.parse` reviver walk: recurse into children first, then call
9117/// `reviver(key, value)`; a returned `undefined` drops the property.
9118///
9119/// The reviver runs with the HOLDER as `this` (25.5.1.1
9120/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9121/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9122/// so `this` was undefined and a reviver could not reach its siblings.
9123/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9124/// VERBATIM, so a number too large for a `double` survives a round trip
9125/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9126///
9127/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9128/// across the whole matrix, is
9129///
9130/// ```text
9131/// ""                 -> SyntaxError: Invalid value for JSON.rawJSON
9132/// leading whitespace -> the parse error for that first character
9133/// a complete literal -> ok
9134/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9135/// a broken literal   -> the parse error the scanner raised
9136/// ```
9137///
9138/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9139/// invalid-value message even though `JSON.parse` accepts the former and gives
9140/// a token error for the latter.
9141fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9142    const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9143    let s = with_host(|h| h.str_of(&arg0(&args)));
9144    if s.is_empty() {
9145        return Err(INVALID.into());
9146    }
9147    let mut p = JsonParser {
9148        chars: s.chars().collect(),
9149        pos: 0,
9150        prims: Vec::new(),
9151        record: false,
9152    };
9153    // An object or an array is rejected where it starts, as leading whitespace
9154    // is — both are "not a primitive", but node reports the token.
9155    if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9156        return Err(p.err_token(0));
9157    }
9158    p.parse_value()?;
9159    if p.pos != p.chars.len() {
9160        // A digit butted against a completed number is still in the number
9161        // scanner, so `"01"` reports the scanner's error rather than leftover
9162        // input — the same distinction `json_parse` draws for trailing text.
9163        if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9164            return Err(p.err_at("Unexpected number", p.pos));
9165        }
9166        return Err(INVALID.into());
9167    }
9168    // A null prototype and one own `rawJSON` property, frozen — the brand is a
9169    // hidden slot so `Object.keys` stays `["rawJSON"]`.
9170    Ok(with_host(|h| {
9171        let mut m: IndexMap<String, Value> = IndexMap::new();
9172        let text = h.new_str(s);
9173        m.insert("rawJSON".into(), text);
9174        let o = h.new_object(m);
9175        let null = h.null();
9176        h.set_proto(&o, null);
9177        h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9178        h.seal_object(&o, true);
9179        o
9180    }))
9181}
9182
9183/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9184/// NOT one, which is why the marker is a hidden slot rather than the property.
9185fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9186    Ok(Value::Bool(is_raw_json(&arg0(&args))))
9187}
9188
9189fn is_raw_json(v: &Value) -> bool {
9190    with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9191}
9192
9193fn json_revive(
9194    key: &str,
9195    val: Value,
9196    reviver: &Value,
9197    holder: &Value,
9198    prims: &[String],
9199    next: &mut usize,
9200) -> Result<Value, String> {
9201    // A PRIMITIVE claims the next recorded source slice before its children
9202    // would — it has none — and a container claims nothing. The walk descends in
9203    // the same order the parse produced them, so one cursor lines the two up.
9204    let is_container =
9205        with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9206    let source = if !is_container {
9207        let s = prims.get(*next).cloned();
9208        if s.is_some() {
9209            *next += 1;
9210        }
9211        s
9212    } else {
9213        None
9214    };
9215    match with_host(|h| h.get(&val).cloned()) {
9216        Some(JsObj::Array(items)) => {
9217            for i in 0..items.len() {
9218                let elem = with_host(|h| match h.get(&val) {
9219                    Some(JsObj::Array(it)) => it[i].clone(),
9220                    _ => Value::Undef,
9221                });
9222                let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9223                with_host(|h| {
9224                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9225                        it[i] = nv;
9226                    }
9227                });
9228            }
9229        }
9230        Some(JsObj::Object(props)) => {
9231            let keys: Vec<String> = props
9232                .keys()
9233                .filter(|k| !k.starts_with("@@"))
9234                .cloned()
9235                .collect();
9236            for k in keys {
9237                let elem = with_host(|h| match h.get(&val) {
9238                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9239                    _ => Value::Undef,
9240                });
9241                let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9242                with_host(|h| {
9243                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9244                        if matches!(nv, Value::Undef) {
9245                            p.shift_remove(&k);
9246                        } else {
9247                            p.insert(k.clone(), nv);
9248                        }
9249                    }
9250                });
9251            }
9252        }
9253        _ => {}
9254    }
9255    let kv = with_host(|h| h.new_str(key.to_string()));
9256    // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9257    // primitive, an empty object for an array or an object — node passes it
9258    // either way, and code reading `ctx.source` used to die on `undefined`
9259    // because only two arguments were passed.
9260    let ctx = with_host(|h| {
9261        let mut m: IndexMap<String, Value> = IndexMap::new();
9262        if let Some(s) = source {
9263            let sv = h.new_str(s);
9264            m.insert("source".into(), sv);
9265        }
9266        h.new_object(m)
9267    });
9268    host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9269}
9270
9271struct JsonParser {
9272    chars: Vec<char>,
9273    pos: usize,
9274    /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9275    /// third argument reports as `context.source` (25.5.1.1). Only collected
9276    /// when a reviver was supplied.
9277    ///
9278    /// A flat list rather than a parallel tree because the reviver walk visits
9279    /// primitives in the same depth-first order the parse produced them, so an
9280    /// index into this is enough to line them up.
9281    prims: Vec<String>,
9282    record: bool,
9283}
9284impl JsonParser {
9285    fn peek(&self) -> Option<char> {
9286        self.chars.get(self.pos).copied()
9287    }
9288
9289    /// `at position N (line L column C)` — the location suffix V8 appends to the
9290    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9291    /// node-js counts `char`s, which agree for the BMP.
9292    fn at(&self, pos: usize) -> String {
9293        let mut line = 1usize;
9294        let mut col = 1usize;
9295        for c in &self.chars[..pos.min(self.chars.len())] {
9296            if *c == '\n' {
9297                line += 1;
9298                col = 1;
9299            } else {
9300                col += 1;
9301            }
9302        }
9303        format!(" at position {pos} (line {line} column {col})")
9304    }
9305
9306    /// A positional error (`Expected ':' after property name in JSON at …`).
9307    fn err_at(&self, what: &str, pos: usize) -> String {
9308        format!("SyntaxError: {what} in JSON{}", self.at(pos))
9309    }
9310
9311    /// The one positional message V8 does NOT suffix with `in JSON`.
9312    fn err_trailing(&self, pos: usize) -> String {
9313        format!(
9314            "SyntaxError: Unexpected non-whitespace character after JSON{}",
9315            self.at(pos)
9316        )
9317    }
9318
9319    /// V8's default parse error: the offending character plus a window of the
9320    /// source. The whole input is quoted when it is short (<= 20 chars);
9321    /// otherwise a 10-character context window either side of `pos` is shown,
9322    /// elided with `...` on whichever side was cut.
9323    fn err_token(&self, pos: usize) -> String {
9324        const MAX_WHOLE: usize = 20;
9325        const CONTEXT: usize = 10;
9326        let len = self.chars.len();
9327        let Some(c) = self.chars.get(pos) else {
9328            return "SyntaxError: Unexpected end of JSON input".into();
9329        };
9330        // V8 reports the whole input for the JS literals that are famously not
9331        // JSON, without naming an offending character.
9332        let whole: String = self.chars.iter().collect();
9333        if matches!(
9334            whole.as_str(),
9335            "undefined" | "NaN" | "Infinity" | "-Infinity"
9336        ) {
9337            return format!("SyntaxError: \"{whole}\" is not valid JSON");
9338        }
9339        let snippet = if len <= MAX_WHOLE {
9340            format!("\"{whole}\"")
9341        } else {
9342            let start = pos.saturating_sub(CONTEXT);
9343            let end = (pos + CONTEXT).min(len);
9344            let body: String = self.chars[start..end].iter().collect();
9345            let head = if start > 0 { "..." } else { "" };
9346            let tail = if end < len { "..." } else { "" };
9347            format!("{head}\"{body}\"{tail}")
9348        };
9349        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9350    }
9351
9352    fn skip_ws(&mut self) {
9353        while matches!(
9354            self.peek(),
9355            Some(' ') | Some('\n') | Some('\t') | Some('\r')
9356        ) {
9357            self.pos += 1;
9358        }
9359    }
9360    fn parse_value(&mut self) -> Result<Value, String> {
9361        self.skip_ws();
9362        let start = self.pos;
9363        let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9364        let v = match self.peek() {
9365            Some('{') => self.parse_object(),
9366            Some('[') => self.parse_array(),
9367            Some('"') => {
9368                let s = self.parse_string()?;
9369                Ok(with_host(|h| h.new_str(s)))
9370            }
9371            Some('t') | Some('f') => self.parse_bool(),
9372            Some('n') => {
9373                self.expect_lit("null")?;
9374                Ok(with_host(|h| h.null()))
9375            }
9376            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9377            None => Err("SyntaxError: Unexpected end of JSON input".into()),
9378            _ => Err(self.err_token(self.pos)),
9379        }?;
9380        if prim && self.record {
9381            self.prims
9382                .push(self.chars[start..self.pos].iter().collect());
9383        }
9384        Ok(v)
9385    }
9386    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9387        for ch in lit.chars() {
9388            match self.peek() {
9389                Some(c) if c == ch => self.pos += 1,
9390                // V8 reports the first character that broke the literal, which is
9391                // why `foo` complains about `'o'` (index 2) and not `'f'`.
9392                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9393                _ => return Err(self.err_token(self.pos)),
9394            }
9395        }
9396        Ok(())
9397    }
9398    fn parse_bool(&mut self) -> Result<Value, String> {
9399        if self.peek() == Some('t') {
9400            self.expect_lit("true")?;
9401            Ok(Value::Bool(true))
9402        } else {
9403            self.expect_lit("false")?;
9404            Ok(Value::Bool(false))
9405        }
9406    }
9407    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9408    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9409    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9410    /// it. Each way the grammar can run out has its own message.
9411    fn parse_number(&mut self) -> Result<Value, String> {
9412        let start = self.pos;
9413        if self.peek() == Some('-') {
9414            self.pos += 1;
9415            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9416                return Err(self.err_at("No number after minus sign", self.pos));
9417            }
9418        }
9419        if self.peek() == Some('0') {
9420            self.pos += 1;
9421        } else {
9422            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9423                self.pos += 1;
9424            }
9425        }
9426        if self.peek() == Some('.') {
9427            self.pos += 1;
9428            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9429                return Err(self.err_at("Unterminated fractional number", self.pos));
9430            }
9431            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9432                self.pos += 1;
9433            }
9434        }
9435        if matches!(self.peek(), Some('e') | Some('E')) {
9436            self.pos += 1;
9437            if matches!(self.peek(), Some('+') | Some('-')) {
9438                self.pos += 1;
9439            }
9440            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9441                return Err(self.err_at("Exponent part is missing a number", self.pos));
9442            }
9443            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9444                self.pos += 1;
9445            }
9446        }
9447        let s: String = self.chars[start..self.pos].iter().collect();
9448        s.parse::<f64>()
9449            .map(Value::Float)
9450            .map_err(|_| self.err_at("Unexpected number", start))
9451    }
9452    fn parse_string(&mut self) -> Result<String, String> {
9453        self.pos += 1; // opening quote
9454        let mut out = String::new();
9455        loop {
9456            match self.peek() {
9457                None => return Err(self.err_at("Unterminated string", self.pos)),
9458                Some('"') => {
9459                    self.pos += 1;
9460                    break;
9461                }
9462                Some('\\') => {
9463                    self.pos += 1;
9464                    match self.peek() {
9465                        Some('n') => out.push('\n'),
9466                        Some('t') => out.push('\t'),
9467                        Some('r') => out.push('\r'),
9468                        Some('"') => out.push('"'),
9469                        Some('\\') => out.push('\\'),
9470                        Some('/') => out.push('/'),
9471                        Some('b') => out.push('\u{08}'),
9472                        Some('f') => out.push('\u{0C}'),
9473                        Some('u') => {
9474                            let h: String = self.chars
9475                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9476                                .iter()
9477                                .collect();
9478                            if let Ok(n) = u32::from_str_radix(&h, 16) {
9479                                if let Some(ch) = char::from_u32(n) {
9480                                    out.push(ch);
9481                                }
9482                            }
9483                            self.pos += 4;
9484                        }
9485                        _ => {}
9486                    }
9487                    self.pos += 1;
9488                }
9489                // A raw control character is not legal inside a JSON string; it
9490                // has to be escaped. V8 rejects it rather than passing it through.
9491                Some(c) if (c as u32) < 0x20 => {
9492                    return Err(self.err_at("Bad control character in string literal", self.pos))
9493                }
9494                Some(c) => {
9495                    out.push(c);
9496                    self.pos += 1;
9497                }
9498            }
9499        }
9500        Ok(out)
9501    }
9502    fn parse_array(&mut self) -> Result<Value, String> {
9503        self.pos += 1; // [
9504        let mut items = Vec::new();
9505        self.skip_ws();
9506        if self.peek() == Some(']') {
9507            self.pos += 1;
9508            return Ok(with_host(|h| h.new_array(items)));
9509        }
9510        loop {
9511            items.push(self.parse_value()?);
9512            self.skip_ws();
9513            match self.peek() {
9514                Some(',') => {
9515                    self.pos += 1;
9516                }
9517                Some(']') => {
9518                    self.pos += 1;
9519                    break;
9520                }
9521                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9522            }
9523        }
9524        Ok(with_host(|h| h.new_array(items)))
9525    }
9526    fn parse_object(&mut self) -> Result<Value, String> {
9527        self.pos += 1; // {
9528        let mut props: IndexMap<String, Value> = IndexMap::new();
9529        self.skip_ws();
9530        if self.peek() == Some('}') {
9531            self.pos += 1;
9532            return Ok(with_host(|h| h.new_object(props)));
9533        }
9534        loop {
9535            self.skip_ws();
9536            if self.peek() != Some('"') {
9537                // The first key uses the "or '}'" wording (an empty object is
9538                // still legal there); a key after a comma does not. End of input
9539                // reports the same expectation, at the end position.
9540                return Err(if props.is_empty() {
9541                    self.err_at("Expected property name or '}'", self.pos)
9542                } else {
9543                    self.err_at("Expected double-quoted property name", self.pos)
9544                });
9545            }
9546            let key = self.parse_string()?;
9547            self.skip_ws();
9548            if self.peek() != Some(':') {
9549                return Err(match self.peek() {
9550                    None => "SyntaxError: Unexpected end of JSON input".into(),
9551                    _ => self.err_at("Expected ':' after property name", self.pos),
9552                });
9553            }
9554            self.pos += 1;
9555            let val = self.parse_value()?;
9556            props.insert(key, val);
9557            self.skip_ws();
9558            match self.peek() {
9559                Some(',') => {
9560                    self.pos += 1;
9561                }
9562                Some('}') => {
9563                    self.pos += 1;
9564                    break;
9565                }
9566                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9567            }
9568        }
9569        Ok(with_host(|h| h.new_object(props)))
9570    }
9571}
9572
9573// ══ type methods (array / string / number) ═══════════════════════════════════
9574
9575fn is_array_method(name: &str) -> bool {
9576    matches!(
9577        name,
9578        "push"
9579            | "pop"
9580            | "shift"
9581            | "unshift"
9582            | "map"
9583            | "filter"
9584            | "forEach"
9585            | "join"
9586            | "slice"
9587            | "indexOf"
9588            | "lastIndexOf"
9589            | "includes"
9590            | "reduce"
9591            | "concat"
9592            | "reverse"
9593            | "sort"
9594            | "find"
9595            | "findIndex"
9596            | "some"
9597            | "every"
9598            | "flat"
9599            | "fill"
9600            | "splice"
9601            | "keys"
9602            | "values"
9603            | "entries"
9604            | "flatMap"
9605            | "at"
9606            | "toString"
9607            | "reduceRight"
9608            | "findLast"
9609            | "findLastIndex"
9610            | "copyWithin"
9611    )
9612}
9613/// Every `String.prototype` method node-js implements.
9614///
9615/// A LIST rather than a `matches!` arm because the same set has to be installed
9616/// on the real `String.prototype` object: a method read off the prototype
9617/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9618/// found nothing there, so the two views of "which methods exist" would drift
9619/// if they were written twice.
9620pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9621    "toUpperCase",
9622    "toLowerCase",
9623    "charAt",
9624    "charCodeAt",
9625    "codePointAt",
9626    "indexOf",
9627    "lastIndexOf",
9628    "includes",
9629    "slice",
9630    "substring",
9631    "substr",
9632    "split",
9633    "trim",
9634    "trimStart",
9635    "trimEnd",
9636    "replace",
9637    "replaceAll",
9638    "repeat",
9639    "startsWith",
9640    "endsWith",
9641    "padStart",
9642    "padEnd",
9643    "concat",
9644    "at",
9645    "toString",
9646    "toLocaleString",
9647    "valueOf",
9648    "match",
9649    "matchAll",
9650    "search",
9651    "normalize",
9652    "localeCompare",
9653    "toLocaleUpperCase",
9654    "toLocaleLowerCase",
9655    "isWellFormed",
9656    "toWellFormed",
9657];
9658
9659fn is_string_method(name: &str) -> bool {
9660    STRING_PROTO_METHODS.contains(&name)
9661}
9662
9663/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9664/// spelled the way this frontend spells the key (`@@iterator`).
9665///
9666/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9667/// .prototype`) installs its methods from a list, and only the string-keyed
9668/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9669/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9670/// `Builtin` namespace and its table gate, answered a function. Derived from
9671/// the table rather than written out, so it cannot name a method node does not
9672/// define nor miss one it does.
9673pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9674    let prefix = format!("@proto:{ctor}:");
9675    crate::arity::BUILTIN_ARITY
9676        .iter()
9677        .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9678        .filter(|m| m.starts_with("@@"))
9679        .collect()
9680}
9681
9682/// The builtin constructors whose `.prototype` object is BRANDED — every other
9683/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9684///
9685/// Measured on node v26.8.1 over every constructor this frontend knows:
9686///
9687/// ```text
9688/// Array/Object/Number/String/Boolean/Function   the ES5 legacy slot prototypes
9689/// Symbol/BigInt/Map/Set/WeakMap/WeakSet         carry an own @@toStringTag
9690/// Promise/Iterator/ArrayBuffer/DataView         "
9691/// WeakRef/FinalizationRegistry/URL              "
9692/// URLSearchParams/TextEncoder/TextDecoder       "
9693/// Date/RegExp/Error/TypeError/Uint8Array/…      [object Object]
9694/// ```
9695///
9696/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9697/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9698/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9699/// its own failure message where node names `[object Object]`.
9700pub(crate) const BRANDED_PROTOS: &[&str] = &[
9701    "Array",
9702    "ArrayBuffer",
9703    "BigInt",
9704    "Boolean",
9705    "DataView",
9706    "FinalizationRegistry",
9707    "Function",
9708    "Iterator",
9709    "Map",
9710    "Number",
9711    "Object",
9712    "Promise",
9713    "Set",
9714    "SharedArrayBuffer",
9715    "String",
9716    "Symbol",
9717    "TextDecoder",
9718    "TextEncoder",
9719    "URL",
9720    "URLSearchParams",
9721    "WeakMap",
9722    "WeakRef",
9723    "WeakSet",
9724];
9725
9726/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9727/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9728/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9729///
9730/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9731/// than coercing `o` to a pattern — the protocol every regexp-like library
9732/// implements. None of the five were consulted, so a custom matcher was
9733/// silently stringified instead.
9734fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9735    if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9736        return None;
9737    }
9738    let f = get_property(arg, sym).ok()?;
9739    with_host(|h| host::is_callable(h, &f)).then_some(f)
9740}
9741
9742fn is_regexp_arg(v: &Value) -> bool {
9743    // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9744    // itself a regexp — or a real one can disown the label. Only the heap kind
9745    // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9746    // the TypeError the spec requires.
9747    if let Ok(m) = get_property(v, "@@match") {
9748        if !matches!(m, Value::Undef) {
9749            return with_host(|h| h.truthy(&m));
9750        }
9751    }
9752    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9753}
9754
9755/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9756/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9757fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9758    if pat.is_empty() {
9759        return Ok(s.to_string());
9760    }
9761    let mut out = String::new();
9762    let mut rest = s;
9763    let mut base = 0usize;
9764    while let Some(pos) = rest.find(pat) {
9765        out.push_str(&rest[..pos]);
9766        let offset = base + pos;
9767        let m = with_host(|h| h.new_str(pat.to_string()));
9768        let str_arg = with_host(|h| h.new_str(s.to_string()));
9769        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9770        out.push_str(&with_host(|h| h.str_of(&r)));
9771        let consumed = pos + pat.len();
9772        base += consumed;
9773        rest = &rest[consumed..];
9774        if !all {
9775            break;
9776        }
9777    }
9778    out.push_str(rest);
9779    Ok(out)
9780}
9781/// Every `Number.prototype` method node-js implements — a list for the same
9782/// reason [`STRING_PROTO_METHODS`] is one.
9783pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9784    "toFixed",
9785    "toExponential",
9786    "toString",
9787    "toPrecision",
9788    "toLocaleString",
9789    "valueOf",
9790];
9791
9792fn is_number_method(name: &str) -> bool {
9793    NUMBER_PROTO_METHODS.contains(&name)
9794}
9795
9796/// The exotic kinds whose own dispatch table does NOT already reach the
9797/// `Object.prototype` methods, so the inherited ones have to be routed to.
9798///
9799/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9800/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9801/// it to the object form made `(255).toString(16)` report `[object Number]`.
9802fn inherits_object_methods(recv: &Value) -> bool {
9803    matches!(
9804        with_host(|h| h.kind_of(recv)),
9805        Some(
9806            ObjKind::Map
9807                | ObjKind::Set
9808                | ObjKind::Promise
9809                | ObjKind::RegExp
9810                | ObjKind::Generator
9811                | ObjKind::Symbol
9812                | ObjKind::BigInt
9813                | ObjKind::Iter
9814        )
9815    )
9816}
9817
9818/// Whether `recv`'s own prototype defines `name`, shadowing the
9819/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9820/// `Map.prototype` does not.
9821fn overrides_object_method(recv: &Value, name: &str) -> bool {
9822    match with_host(|h| h.kind_of(recv)) {
9823        Some(ObjKind::Map) => is_map_method(name),
9824        Some(ObjKind::Set) => is_set_method(name),
9825        Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9826        // A Symbol has its own `toString`; `valueOf` is the inherited one,
9827        // which returns the receiver — exactly what a symbol needs.
9828        Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9829        Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9830        _ => false,
9831    }
9832}
9833
9834/// Dispatch `recv.name(args)` for the built-in prototype methods.
9835pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9836    // A USER method on the receiver's prototype chain wins over the builtin of
9837    // the same name — that is how a `class X extends Array` method is reached,
9838    // since the dispatch below goes straight to the builtin table and has no
9839    // entry for it.
9840    //
9841    // Deliberately restricted to a user function: the shared `Object.prototype`
9842    // carries real `@proto:Object:*` thunks, so accepting any callable made a
9843    // bare `map.toString()` resolve to the object form instead of the builtin
9844    // one the exotic is supposed to use.
9845    if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9846        if matches!(
9847            with_host(|h| h.kind_of(&f)),
9848            Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9849        ) {
9850            return host::invoke(&f, args, Some(recv.clone()));
9851        }
9852    }
9853    // A method synthesized from the receiver's KIND is unreachable once its
9854    // intrinsic prototype is off the chain. The read already answers
9855    // `undefined` for one; dispatch has its own table and would still have
9856    // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9857    // while `a.join` was `undefined` — the read and the call disagreeing again,
9858    // in the opposite direction from the monkey-patch case below.
9859    if !own_intrinsic_reachable(recv)
9860        && inherited_method_owner(recv, name).is_none()
9861        && !has_own_for_shadow(recv, name)
9862        && inherited_builtin_static(recv, name).is_none()
9863        && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9864    {
9865        return Err(host::type_error(&format!("{name} is not a function")));
9866    }
9867    // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9868    // path resolves these, but dispatch goes straight to the builtin table and
9869    // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9870    // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9871    // about the same name, on the one path a polyfill actually uses.
9872    if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9873        if let Some(f) = inherited_builtin_static(recv, name) {
9874            if with_host(|h| host::is_callable(h, &f)) {
9875                return host::invoke(&f, args, Some(recv.clone()));
9876            }
9877        }
9878    }
9879    // Every object INHERITS the `Object.prototype` methods, and an exotic that
9880    // does not define its own reaches them the same way. Each kind's dispatch
9881    // table below only knows its own methods, so `new Map().toString()`,
9882    // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9883    // not a function" — `Object.prototype.toString.call(m)` worked while
9884    // `m.toString()` did not.
9885    // The allowlist is the kinds whose own dispatch table below would otherwise
9886    // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9887    // the same way — a function, a class and a bound function included, where
9888    // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9889    // resolved it. `inherited_method_owner` decides which prototype owns the
9890    // name, so a kind that defines its own still gets its own.
9891    if is_object_builtin_method(name)
9892        && (inherited_method_owner(recv, name) == Some("Object")
9893            || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9894    {
9895        // `toString` goes through the branded form (20.1.3.6), which reads
9896        // `Symbol.toStringTag` and falls back to the receiver's own brand —
9897        // `[object Map]`, not the generic stringification.
9898        if name == "toString" {
9899            return proto_method(recv, "Object:toString", args);
9900        }
9901        return object_builtin_method(recv, name, args);
9902    }
9903    // `Object.prototype.valueOf` is inherited by every exotic that does not
9904    // override it (an Array does not), and returns the receiver. Without this
9905    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9906    // and threw `valueOf is not a function`.
9907    if name == "valueOf"
9908        && matches!(
9909            with_host(|h| h.kind_of(recv)),
9910            Some(
9911                ObjKind::Array
9912                    | ObjKind::Map
9913                    | ObjKind::Set
9914                    | ObjKind::Generator
9915                    | ObjKind::Promise
9916                    | ObjKind::Iter
9917                    | ObjKind::RegExp
9918            )
9919        )
9920    {
9921        return Ok(recv.clone());
9922    }
9923    // Only the tag is needed to pick the branch — cloning the receiver here made
9924    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9925    match with_host(|h| h.kind_of(recv)) {
9926        Some(ObjKind::Array) => array_method(recv, name, args),
9927        Some(ObjKind::Str) => {
9928            // `string_method` consumes the text itself, so this clone is the
9929            // payload, not a tag probe.
9930            let s = peek(recv, |o| match o {
9931                JsObj::Str(s) => Some(s.clone()),
9932                _ => None,
9933            })
9934            .unwrap_or_default();
9935            string_method(&s, name, args)
9936        }
9937        Some(ObjKind::Map) => map_method(recv, name, args),
9938        Some(ObjKind::Set) => set_method(recv, name, args),
9939        Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9940            crate::stdlib::iterator::call(recv, name, &args)
9941        }
9942        Some(ObjKind::Generator) => generator_method(recv, name, args),
9943        Some(ObjKind::Promise) => promise_method(recv, name, args),
9944        Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9945            crate::stdlib::iterator::call(recv, name, &args)
9946        }
9947        Some(ObjKind::Iter) => iter_method(recv, name, args),
9948        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9949        Some(ObjKind::BigInt) => {
9950            let b = peek(recv, |o| match o {
9951                JsObj::BigInt(b) => Some(b.clone()),
9952                _ => None,
9953            })
9954            .unwrap_or_default();
9955            bigint_method(&b, name, args)
9956        }
9957        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9958        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9959            match function_builtin_method(recv, name, &args)? {
9960                Some(v) => Ok(v),
9961                None => Err(host::type_error(&format!("{name} is not a function"))),
9962            }
9963        }
9964        Some(ObjKind::Object) => {
9965            if let Some(f) = peek(recv, |o| match o {
9966                JsObj::Object(p) => p.get(name).cloned(),
9967                _ => None,
9968            }) {
9969                host::invoke(&f, args, Some(recv.clone()))
9970            } else if name == "hasOwnProperty" {
9971                let k = with_host(|h| h.str_of(&arg0(&args)));
9972                let has = peek(recv, |o| match o {
9973                    JsObj::Object(p) => Some(p.contains_key(&k)),
9974                    _ => None,
9975                })
9976                .unwrap_or(false);
9977                Ok(Value::Bool(has))
9978            } else if name == "toString" {
9979                Ok(with_host(|h| h.new_str("[object Object]")))
9980            } else {
9981                Err(host::type_error(&format!("{} is not a function", name)))
9982            }
9983        }
9984        _ => {
9985            // Primitive number/bool/string coercions.
9986            if let Value::Float(_) | Value::Int(_) = recv {
9987                return number_method(with_host(|h| h.to_number(recv)), name, args);
9988            }
9989            if let Some(s) = with_host(|h| h.as_str(recv)) {
9990                return string_method(&s, name, args);
9991            }
9992            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9993            // so it reached no branch at all and `true.toString()` threw `is not
9994            // a function`. Its three methods are `toString`, `valueOf`, and the
9995            // inherited `Object.prototype.toLocaleString` — which
9996            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9997            // was reachable from the array form too.
9998            if let Value::Bool(b) = recv {
9999                return match name {
10000                    "toString" | "toLocaleString" => {
10001                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
10002                    }
10003                    "valueOf" => Ok(Value::Bool(*b)),
10004                    _ => Err(host::type_error(&format!("{name} is not a function"))),
10005                };
10006            }
10007            Err(host::type_error(&format!("{} is not a function", name)))
10008        }
10009    }
10010}
10011
10012/// A copy of the whole backing store, for the methods that genuinely consume
10013/// every element (`map`, `filter`, `join`, …). Never call it just to read
10014/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10015/// A LIVE iterator over a `Map` or `Set`.
10016///
10017/// Node's collection iterators see the collection as it is at each step: an
10018/// entry added during iteration IS visited, and one deleted before it is
10019/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10020/// a loop that deletes as it goes still processed the entries it had removed.
10021///
10022/// The cursor is the last key yielded plus the index it was at. On each step
10023/// the key is located again in the CURRENT order: if it is still there the next
10024/// entry follows it, and if it was itself deleted the stored index now names
10025/// the entry that shifted into its place. That reproduces node for the cases
10026/// its own tests turn on — add-during, delete-ahead, delete-self,
10027/// delete-behind, delete-the-rest and clear — without giving `Map` the
10028/// tombstoned entry list node uses internally.
10029fn collection_iterator(coll: &Value, kind: &str) -> Value {
10030    with_host(|h| {
10031        let mut m = IndexMap::new();
10032        m.insert(
10033            "@@native".into(),
10034            h.new_str("CollectionIterator".to_string()),
10035        );
10036        m.insert("@@coll".into(), coll.clone());
10037        m.insert("@@kind".into(), h.new_str(kind.to_string()));
10038        m.insert("@@started".into(), Value::Bool(false));
10039        m.insert("@@lastIdx".into(), Value::Float(0.0));
10040        h.new_object(m)
10041    })
10042}
10043
10044/// One step of a live collection iterator.
10045pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10046    let slot = |k: &str| {
10047        with_host(|h| match h.get(recv) {
10048            Some(JsObj::Object(p)) => p.get(k).cloned(),
10049            _ => None,
10050        })
10051    };
10052    let coll = slot("@@coll").unwrap_or(Value::Undef);
10053    let kind = slot("@@kind")
10054        .map(|v| with_host(|h| h.str_of(&v)))
10055        .unwrap_or_default();
10056    let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10057    let last_idx = slot("@@lastIdx")
10058        .map(|v| with_host(|h| h.to_number(&v)) as usize)
10059        .unwrap_or(0);
10060    let last_key = slot("@@lastKey");
10061
10062    let next_idx = if !started {
10063        0
10064    } else {
10065        match last_key
10066            .as_ref()
10067            .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10068        {
10069            // Still present: continue after it.
10070            Some(i) => i + 1,
10071            // Deleted since: whatever shifted into its slot is next.
10072            None => last_idx,
10073        }
10074    };
10075    let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10076    let Some((k, v)) = entry else {
10077        return Ok(iter_result(Value::Undef, true));
10078    };
10079    with_host(|h| {
10080        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10081            p.insert("@@started".into(), Value::Bool(true));
10082            p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10083            p.insert("@@lastKey".into(), k.clone());
10084        }
10085    });
10086    let out = match kind.as_str() {
10087        "keys" => k,
10088        "values" => v,
10089        _ => with_host(|h| h.new_array(vec![k, v])),
10090    };
10091    Ok(iter_result(out, false))
10092}
10093
10094/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10095fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10096    match h.get(coll) {
10097        Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10098        Some(JsObj::Set { entries, .. }) => {
10099            entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10100        }
10101        _ => None,
10102    }
10103}
10104
10105/// Where `key` currently sits in the collection's order.
10106fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10107    let mk = host::map_key(h, key);
10108    match h.get(coll) {
10109        Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10110        Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10111        _ => None,
10112    }
10113}
10114
10115/// The `thisArg` an iteration method was given, if any.
10116///
10117/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10118/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10119/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10120/// `Array.from`'s map function. Every one of them was invoking the callback
10121/// with no receiver, so `this` inside it was undefined and the argument did
10122/// nothing.
10123fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10124    args.get(idx)
10125        .filter(|v| !matches!(v, Value::Undef))
10126        .cloned()
10127}
10128
10129/// The elements of an array, with any INDEX ACCESSOR resolved.
10130///
10131/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10132/// table, and an array's elements live in a backing vector — so every method
10133/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10134/// slot and never called the getter, while a plain `arr[1]` read did.
10135///
10136/// An array with no accessors pays one lookup returning an empty list, so the
10137/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10138/// since calling one re-enters.
10139/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10140/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10141/// read LIVE at its index, and an index that no longer exists is skipped.
10142///
10143/// Snapshotting the whole array instead meant a callback that mutated it was
10144/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10145/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10146///
10147/// `f` returns `Some(x)` to stop early with `x`.
10148fn array_walk<T>(
10149    recv: &Value,
10150    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10151) -> Result<Option<T>, String> {
10152    let len = array_len(recv);
10153    for i in 0..len {
10154        // A HOLE — and an index a shrinking mutation has dropped — is skipped
10155        // without calling the callback.
10156        if index_absent(recv, i) || i >= array_len(recv) {
10157            continue;
10158        }
10159        let v = get_property(recv, &i.to_string())?;
10160        if let Some(out) = f(i, v)? {
10161            return Ok(Some(out));
10162        }
10163    }
10164    Ok(None)
10165}
10166
10167/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10168/// capture-length-once, read-each-element-live rule walked from the end. A
10169/// callback that SHRINKS the array is observed by every later step, so the
10170/// indices it drops are skipped rather than served from a stale copy.
10171fn array_walk_rev<T>(
10172    recv: &Value,
10173    from: usize,
10174    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10175) -> Result<Option<T>, String> {
10176    for i in (0..from).rev() {
10177        if index_absent(recv, i) || i >= array_len(recv) {
10178            continue;
10179        }
10180        let v = get_property(recv, &i.to_string())?;
10181        if let Some(out) = f(i, v)? {
10182            return Ok(Some(out));
10183        }
10184    }
10185    Ok(None)
10186}
10187
10188/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10189/// `undefined` once a mutation has shrunk the array past it.
10190fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10191    if i >= array_len(recv) {
10192        return Ok(Value::Undef);
10193    }
10194    get_property(recv, &i.to_string())
10195}
10196
10197fn array_items(recv: &Value) -> Vec<Value> {
10198    let mut items = with_host(|h| match h.get(recv) {
10199        Some(JsObj::Array(items)) => items.clone(),
10200        _ => Vec::new(),
10201    });
10202    resolve_index_accessors(recv, &mut items);
10203    items
10204}
10205
10206/// Replace each slot that has an own accessor with what its getter returns.
10207pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10208    resolve_index_accessors(recv, items);
10209}
10210
10211/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10212/// the ORIGINAL array when nothing changed, and the original still holds the
10213/// stale slots.
10214fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10215    let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10216        .into_iter()
10217        .filter_map(|k| k.parse::<usize>().ok())
10218        .filter(|i| *i < items.len())
10219        .collect();
10220    // An ELIDED index the prototype chain supplies is stale in the backing
10221    // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10222    // value. Spread and `JSON.stringify` both read through here, and both
10223    // rendered the hole rather than what `a[i]` reads.
10224    let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10225        .into_iter()
10226        .filter(|i| *i < items.len() && !indices.contains(i))
10227        .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10228        .collect();
10229    indices.extend(inherited);
10230    let mut replaced = false;
10231    for i in indices {
10232        if let Ok(v) = get_property(recv, &i.to_string()) {
10233            items[i] = v;
10234            replaced = true;
10235        }
10236    }
10237    replaced
10238}
10239
10240/// The ELIDED positions of array `recv` as a membership set. A dense array —
10241/// which is nearly every array — answers with an empty set after a single
10242/// negative hash probe and allocates nothing.
10243///
10244/// The iteration methods split into two groups, and the split is not a matter of
10245/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10246/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10247/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10248/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10249fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10250    with_host(|h| h.hole_indices(recv)).into_iter().collect()
10251}
10252
10253/// The indices `recv` genuinely has NO property at — the elided ones the
10254/// prototype chain does not supply either.
10255///
10256/// Every array method tests `HasProperty` before deciding to skip a position
10257/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10258/// alone made an inherited element invisible to all of them: with
10259/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10260/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10261/// the same position.
10262///
10263/// `hole_set` remains the elision record itself, which is what `splice` moves
10264/// around — that bookkeeping is about the array's OWN storage and must not
10265/// consult the chain.
10266fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10267    hole_set(recv)
10268        .into_iter()
10269        .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10270        .collect()
10271}
10272
10273/// The single-index form of [`absent_set`], for the walkers that test one
10274/// position at a time.
10275fn index_absent(recv: &Value, i: usize) -> bool {
10276    with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10277}
10278
10279/// The element count, without copying the elements.
10280fn array_len(recv: &Value) -> usize {
10281    peek(recv, |o| match o {
10282        JsObj::Array(items) => Some(items.len()),
10283        _ => None,
10284    })
10285    .unwrap_or(0)
10286}
10287
10288/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10289/// array method builds its RESULT with.
10290///
10291/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10292/// produce an array of the receiver's own species, so on a `class A extends
10293/// Array` the result is an `A`. Every one of them allocated a plain array
10294/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10295///
10296/// The default `get [Symbol.species]() { return this }` is what makes the
10297/// subclass the species; a class overriding it with `Array` gets a plain array
10298/// back, which is the documented way to opt out.
10299/// Build an array-shaped result through `ctor`, or a plain array when there is
10300/// none to build through.
10301///
10302/// The constructor is called with the LENGTH and the elements written after, as
10303/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10304/// constructor observe the allocation.
10305fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10306    let Some(ctor) = ctor.filter(|c| {
10307        matches!(
10308            with_host(|h| h.kind_of(c)),
10309            Some(ObjKind::Class) | Some(ObjKind::Func)
10310        )
10311    }) else {
10312        return Ok(with_host(|h| h.new_array(items)));
10313    };
10314    let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10315    write_elements(&out, items);
10316    Ok(out)
10317}
10318
10319/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10320/// hole marks the length-only construction left behind.
10321///
10322/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10323/// the elements written over them stayed marked — so every subclass result of
10324/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10325/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10326fn write_elements(out: &Value, items: Vec<Value>) {
10327    with_host(|h| {
10328        h.clear_holes(out);
10329        if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10330            *dst = items;
10331        }
10332    });
10333}
10334
10335fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10336    let plain = || with_host(|h| h.new_array(items.clone()));
10337    // Only a subclass instance can have a species of its own: a plain array's
10338    // `constructor` is the `Array` builtin, whose species is `Array`.
10339    // A chain lookup, not `get_property`: an Array receiver resolves its
10340    // properties through the stdlib funnel, which has no `constructor` entry,
10341    // so the read alone reports `undefined` for every subclass instance. A
10342    // Proxy is the exception — it has no property map to walk, and its
10343    // `constructor` comes from the `get` trap, so a proxied subclass array
10344    // produced plain arrays.
10345    let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10346        get_property(recv, "constructor").unwrap_or(Value::Undef)
10347    } else {
10348        with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10349    };
10350    if !matches!(
10351        with_host(|h| h.kind_of(&ctor)),
10352        Some(ObjKind::Class) | Some(ObjKind::Func)
10353    ) {
10354        return Ok(plain());
10355    }
10356    // An explicit `@@species` wins; absent one, the constructor itself is the
10357    // species, as the inherited accessor returns `this`.
10358    let species = match get_property(&ctor, "@@species") {
10359        Ok(Value::Undef) => ctor,
10360        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10361        Ok(s) => s,
10362        Err(_) => ctor,
10363    };
10364    if !matches!(
10365        with_host(|h| h.kind_of(&species)),
10366        Some(ObjKind::Class) | Some(ObjKind::Func)
10367    ) {
10368        return Ok(plain());
10369    }
10370    let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10371    // The constructor is called with the LENGTH, so the elements are written
10372    // afterwards — which is also what lets a subclass constructor observe the
10373    // allocation, as node's does.
10374    write_elements(&out, items);
10375    Ok(out)
10376}
10377
10378fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10379    array_method_on(recv, recv, name, args)
10380}
10381
10382/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10383/// generic path to copy the result back onto the array-like.
10384const ARRAY_MUTATORS: &[&str] = &[
10385    "push",
10386    "pop",
10387    "shift",
10388    "unshift",
10389    "splice",
10390    "sort",
10391    "reverse",
10392    "fill",
10393    "copyWithin",
10394];
10395
10396/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10397/// a DOM-ish collection, `arguments`).
10398///
10399/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10400/// rather than over an Array's element vector, so the receiver only has to have
10401/// a `length`. The elements are read out into a temporary Array, the ordinary
10402/// implementation runs on that, and a MUTATING method writes the result back —
10403/// which keeps one implementation of each method rather than a second, generic
10404/// one that could drift from it.
10405///
10406/// An index the receiver does not own is a HOLE in the temporary, so the
10407/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10408fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10409    let len = match get_property(recv, "length") {
10410        Ok(v) => host::to_array_length(&v).unwrap_or(0),
10411        Err(_) => 0,
10412    };
10413    // A STRING receiver owns every index of its length; `has_property` answers
10414    // for objects and reports none of them, which made `[].map.call('abc', f)`
10415    // an array of three holes.
10416    let dense = with_host(|h| h.as_str(recv)).is_some();
10417    let mut items = Vec::with_capacity(len);
10418    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10419    for i in 0..len {
10420        let k = i.to_string();
10421        if dense || has_property(recv, &k)? {
10422            items.push(get_property(recv, &k)?);
10423        } else {
10424            holes.insert(i);
10425            items.push(Value::Undef);
10426        }
10427    }
10428    let tmp = with_host(|h| {
10429        let a = h.new_array(items);
10430        h.install_holes(&a, holes);
10431        a
10432    });
10433    let out = array_method_on(&tmp, recv, method, args)?;
10434    if ARRAY_MUTATORS.contains(&method) {
10435        let result = with_host(|h| match h.get(&tmp) {
10436            Some(JsObj::Array(items)) => items.clone(),
10437            _ => Vec::new(),
10438        });
10439        for (i, v) in result.iter().enumerate() {
10440            set_property(recv, &i.to_string(), v.clone())?;
10441        }
10442        set_property(recv, "length", Value::Float(result.len() as f64))?;
10443    }
10444    Ok(out)
10445}
10446
10447/// `Array.prototype.<name>` on `recv`.
10448///
10449/// `this_value` is what a callback receives as its third argument and what a
10450/// mutating method returns — the same object as `recv` for an ordinary array
10451/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10452/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10453fn array_method_on(
10454    recv: &Value,
10455    this_value: &Value,
10456    name: &str,
10457    args: Vec<Value>,
10458) -> Result<Value, String> {
10459    let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10460    match name {
10461        "push" => {
10462            // 23.1.3.23 step 4 defines each new element through
10463            // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10464            // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10465            // TypeError. The elements were appended to the backing vector
10466            // regardless, so sealing an array did not seal it.
10467            if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10468                let at = array_len(recv);
10469                return Err(host::type_error(&format!(
10470                    "Cannot add property {at}, object is not extensible"
10471                )));
10472            }
10473            // Step 5 then SETS `length`, so a non-writable one refuses the push
10474            // too — `defineProperty(a, 'length', {writable: false})` makes an
10475            // array append-proof without sealing it.
10476            if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10477                return Err(host::type_error(
10478                    "Cannot assign to read only property 'length' of object '[object Array]'",
10479                ));
10480            }
10481            // `push` returns the new length; take it from the same mutable
10482            // borrow rather than copying the array back out to count it.
10483            let len = with_host(|h| {
10484                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10485                    items.extend(args.iter().cloned());
10486                    items.len()
10487                } else {
10488                    0
10489                }
10490            });
10491            Ok(Value::Float(len as f64))
10492        }
10493        "pop" => Ok(with_host(|h| {
10494            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10495                items.pop().unwrap_or(Value::Undef)
10496            } else {
10497                Value::Undef
10498            };
10499            let len = match h.get(recv) {
10500                Some(JsObj::Array(items)) => items.len(),
10501                _ => 0,
10502            };
10503            h.truncate_holes(recv, len);
10504            popped
10505        })),
10506        "shift" => Ok(with_host(|h| {
10507            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10508                if items.is_empty() {
10509                    Value::Undef
10510                } else {
10511                    items.remove(0)
10512                }
10513            } else {
10514                Value::Undef
10515            };
10516            h.remap_holes(recv, |i| i.checked_sub(1));
10517            shifted
10518        })),
10519        "unshift" => {
10520            with_host(|h| {
10521                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10522                    for (i, a) in args.iter().enumerate() {
10523                        items.insert(i, a.clone());
10524                    }
10525                }
10526                let n = args.len();
10527                h.remap_holes(recv, |i| Some(i + n));
10528            });
10529            Ok(Value::Float(array_len(recv) as f64))
10530        }
10531        "join" => {
10532            let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10533                ",".to_string()
10534            } else {
10535                arg_to_string(&args, 0)?
10536            };
10537            join_array(recv, &sep)
10538        }
10539        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10540        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10541        // empty string. It threw `is not a function` — the whole method was
10542        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10543        "toLocaleString" => {
10544            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10545            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10546            if !host::join_stack_push(recv) {
10547                return Ok(with_host(|h| h.new_str(String::new())));
10548            }
10549            let items = array_items(recv);
10550            let mut parts: Vec<String> = Vec::with_capacity(items.len());
10551            for it in &items {
10552                if with_host(|h| h.is_nullish(it)) {
10553                    parts.push(String::new());
10554                    continue;
10555                }
10556                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10557                    Ok(v) => v,
10558                    Err(e) => {
10559                        host::join_stack_pop();
10560                        return Err(e);
10561                    }
10562                };
10563                parts.push(with_host(|h| h.str_of(&v)));
10564            }
10565            host::join_stack_pop();
10566            Ok(with_host(|h| h.new_str(parts.join(","))))
10567        }
10568        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10569        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10570        // `Get`-based `includes` reports `true` for the same array.
10571        "indexOf" => {
10572            let target = arg0(&args);
10573            let len = array_len(recv);
10574            let start = search_start(arg_num(&args, 1), len);
10575            let mut idx = None;
10576            for i in start..len {
10577                // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10578                // index a mutation has since dropped — is skipped, not compared.
10579                if index_absent(recv, i) || i >= array_len(recv) {
10580                    continue;
10581                }
10582                let x = get_property(recv, &i.to_string())?;
10583                if with_host(|h| h.strict_eq(&x, &target)) {
10584                    idx = Some(i);
10585                    break;
10586                }
10587            }
10588            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10589        }
10590        "lastIndexOf" => {
10591            let items = array_items(recv);
10592            let holes = absent_set(recv);
10593            let target = arg0(&args);
10594            let from = (args.len() > 1).then(|| arg_num(&args, 1));
10595            let idx = match search_start_last(from, items.len()) {
10596                None => None,
10597                Some(start) => with_host(|h| {
10598                    items[..=start]
10599                        .iter()
10600                        .enumerate()
10601                        .rev()
10602                        .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10603                        .map(|(i, _)| i)
10604                }),
10605            };
10606            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10607        }
10608        "includes" => {
10609            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10610            // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10611            // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10612            let target = arg0(&args);
10613            let tnan = matches!(target, Value::Float(f) if f.is_nan());
10614            let len = array_len(recv);
10615            let start = search_start(arg_num(&args, 1), len);
10616            let mut found = false;
10617            for i in start..len {
10618                let x = array_elem_live(recv, i)?;
10619                if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10620                    || with_host(|h| h.strict_eq(&x, &target))
10621                {
10622                    found = true;
10623                    break;
10624                }
10625            }
10626            Ok(Value::Bool(found))
10627        }
10628        "slice" => {
10629            let items = array_items(recv);
10630            let (lo, hi) = slice_bounds(&args, items.len());
10631            let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10632            with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10633            Ok(out)
10634        }
10635        "concat" => {
10636            // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10637            // is spread, overriding `IsArray` in BOTH directions: a plain
10638            // array-like opts IN, and an array opts OUT.
10639            let spreadable = |a: &Value| -> bool {
10640                let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10641                if matches!(flag, Value::Undef) {
10642                    matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10643                        && !is_arguments(a)
10644                } else {
10645                    with_host(|h| h.truthy(&flag))
10646                }
10647            };
10648            // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10649            // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10650            // element, its `ToObject` box, not the characters `array_generic`
10651            // read out of it. A hole in a spread receiver or argument stays a
10652            // hole in the result, at its shifted position.
10653            let (mut out, mut holes) = if spreadable(this_value) {
10654                (array_items(recv), absent_set(recv))
10655            } else {
10656                (vec![to_object(this_value)], Default::default())
10657            };
10658            let mut sources: Vec<(Value, usize)> = Vec::new();
10659            for a in &args {
10660                if !spreadable(a) {
10661                    out.push(a.clone());
10662                    continue;
10663                }
10664                match with_host(|h| h.get(a).cloned()) {
10665                    // Read off the backing vector rather than through
10666                    // `array_items`, so the resolve that does for the receiver
10667                    // has to be done here too: 23.1.3.1 step 5.c.iv is a
10668                    // `[[Get]]`, and an index with a getter — or an elided one
10669                    // the chain supplies — is stale in that vector.
10670                    Some(JsObj::Array(mut items)) => {
10671                        resolve_index_accessors(a, &mut items);
10672                        sources.push((a.clone(), out.len()));
10673                        out.extend(items);
10674                    }
10675                    // An opted-in array-LIKE spreads by its `length` and index
10676                    // properties rather than by a backing vector it has none of.
10677                    _ => {
10678                        let len = get_property(a, "length").unwrap_or(Value::Undef);
10679                        let n = with_host(|h| h.to_number(&len));
10680                        let n = if n.is_finite() {
10681                            n.max(0.0) as usize
10682                        } else {
10683                            0
10684                        };
10685                        for i in 0..n {
10686                            out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10687                        }
10688                    }
10689                }
10690            }
10691
10692            for (src, base) in sources {
10693                holes.extend(
10694                    with_host(|h| h.hole_indices(&src))
10695                        .into_iter()
10696                        .map(|i| i + base),
10697                );
10698            }
10699            let arr = array_species_create(this_value, out)?;
10700            with_host(|h| h.install_holes(&arr, holes));
10701            Ok(arr)
10702        }
10703        "reverse" => {
10704            let len = array_len(recv);
10705            with_host(|h| {
10706                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10707                    items.reverse();
10708                }
10709                h.remap_holes(recv, |i| Some(len - 1 - i));
10710            });
10711            Ok(this_value.clone())
10712        }
10713        "fill" => {
10714            // fill(value[, start[, end]]) — negative indices count from the end.
10715            let val = arg0(&args);
10716            let len = array_len(recv) as i64;
10717            let norm =
10718                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10719            let start = if args.len() >= 2 {
10720                norm(arg_num(&args, 1) as i64)
10721            } else {
10722                0
10723            };
10724            let end = if args.len() >= 3 {
10725                norm(arg_num(&args, 2) as i64)
10726            } else {
10727                len as usize
10728            };
10729            with_host(|h| {
10730                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10731                    for it in items.iter_mut().take(end).skip(start) {
10732                        *it = val.clone();
10733                    }
10734                }
10735                // Every filled position now holds a real value.
10736                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10737            });
10738            Ok(this_value.clone())
10739        }
10740        "copyWithin" => {
10741            // copyWithin(target, start[, end]) — copy a slice within the array.
10742            let items = array_items(recv);
10743            let len = items.len() as i64;
10744            let norm =
10745                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10746            let target = norm(arg_num(&args, 0) as i64);
10747            let start = if args.len() >= 2 {
10748                norm(arg_num(&args, 1) as i64)
10749            } else {
10750                0
10751            };
10752            let end = if args.len() >= 3 {
10753                norm(arg_num(&args, 2) as i64)
10754            } else {
10755                len as usize
10756            };
10757            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10758            let copied = slice.len();
10759            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10760            // deletes the target when the source has no such property);
10761            // everything outside the written range keeps its own.
10762            let src_holes = absent_set(recv);
10763            with_host(|h| {
10764                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10765                    for (k, v) in slice.into_iter().enumerate() {
10766                        if target + k < a.len() {
10767                            a[target + k] = v;
10768                        }
10769                    }
10770                }
10771                let len = len as usize;
10772                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10773                    .iter()
10774                    .copied()
10775                    .filter(|i| *i < target || *i >= (target + copied).min(len))
10776                    .collect();
10777                for k in 0..copied {
10778                    if target + k < len && src_holes.contains(&(start + k)) {
10779                        holes.insert(target + k);
10780                    }
10781                }
10782                h.install_holes(recv, holes);
10783            });
10784            Ok(this_value.clone())
10785        }
10786        "at" => {
10787            let items = array_items(recv);
10788            let mut i = arg_num(&args, 0) as i64;
10789            if i < 0 {
10790                i += items.len() as i64;
10791            }
10792            Ok(if i >= 0 && (i as usize) < items.len() {
10793                items[i as usize].clone()
10794            } else {
10795                Value::Undef
10796            })
10797        }
10798        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10799        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10800        // twice and yields `[2, <1 empty item>, 6]`.
10801        "map" => {
10802            let holes = absent_set(recv);
10803            let cb = arg0(&args);
10804            // The result keeps the source's LENGTH, so a skipped index still
10805            // occupies a slot; `array_walk` only tells us which ones ran.
10806            let mut out = vec![Value::Undef; array_len(recv)];
10807            array_walk(recv, |i, it| {
10808                let v = host::invoke(
10809                    &cb,
10810                    vec![it, Value::Float(i as f64), this_value.clone()],
10811                    this_arg(&args, 1),
10812                )?;
10813                if i < out.len() {
10814                    out[i] = v;
10815                }
10816                Ok(None::<()>)
10817            })?;
10818            let arr = array_species_create(this_value, out)?;
10819            with_host(|h| h.install_holes(&arr, holes));
10820            Ok(arr)
10821        }
10822        "flatMap" => {
10823            let cb = arg0(&args);
10824            let thisarg = this_arg(&args, 1);
10825            let mut out = Vec::new();
10826            array_walk(recv, |i, v| {
10827                let r = host::invoke(
10828                    &cb,
10829                    vec![v, Value::Float(i as f64), this_value.clone()],
10830                    thisarg.clone(),
10831                )?;
10832                match with_host(|h| h.get(&r).cloned()) {
10833                    Some(JsObj::Array(inner)) => out.extend(inner),
10834                    _ => out.push(r),
10835                }
10836                Ok(None::<()>)
10837            })?;
10838            array_species_create(this_value, out)
10839        }
10840        "filter" => {
10841            let cb = arg0(&args);
10842            let mut out = Vec::new();
10843            array_walk(recv, |i, it| {
10844                let keep = host::invoke(
10845                    &cb,
10846                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10847                    this_arg(&args, 1),
10848                )?;
10849                if with_host(|h| h.truthy(&keep)) {
10850                    out.push(it);
10851                }
10852                Ok(None::<()>)
10853            })?;
10854            array_species_create(this_value, out)
10855        }
10856        "forEach" => {
10857            let cb = arg0(&args);
10858            array_walk(recv, |i, it| {
10859                host::invoke(
10860                    &cb,
10861                    vec![it, Value::Float(i as f64), this_value.clone()],
10862                    this_arg(&args, 1),
10863                )?;
10864                Ok(None::<()>)
10865            })?;
10866            Ok(Value::Undef)
10867        }
10868        "find" => {
10869            let items = array_items(recv);
10870            let cb = arg0(&args);
10871            for (i, it) in items.iter().enumerate() {
10872                let m = host::invoke(
10873                    &cb,
10874                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10875                    this_arg(&args, 1),
10876                )?;
10877                if with_host(|h| h.truthy(&m)) {
10878                    return Ok(it.clone());
10879                }
10880            }
10881            Ok(Value::Undef)
10882        }
10883        "findIndex" => {
10884            let items = array_items(recv);
10885            let cb = arg0(&args);
10886            for (i, it) in items.iter().enumerate() {
10887                let m = host::invoke(
10888                    &cb,
10889                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10890                    this_arg(&args, 1),
10891                )?;
10892                if with_host(|h| h.truthy(&m)) {
10893                    return Ok(Value::Float(i as f64));
10894                }
10895            }
10896            Ok(Value::Float(-1.0))
10897        }
10898        "some" => {
10899            let cb = arg0(&args);
10900            let thisarg = this_arg(&args, 1);
10901            let hit = array_walk(recv, |i, v| {
10902                let m = host::invoke(
10903                    &cb,
10904                    vec![v, Value::Float(i as f64), this_value.clone()],
10905                    thisarg.clone(),
10906                )?;
10907                Ok(with_host(|h| h.truthy(&m)).then_some(()))
10908            })?;
10909            Ok(Value::Bool(hit.is_some()))
10910        }
10911        "every" => {
10912            let cb = arg0(&args);
10913            let failed = array_walk(recv, |i, it| {
10914                let m = host::invoke(
10915                    &cb,
10916                    vec![it, Value::Float(i as f64), this_value.clone()],
10917                    this_arg(&args, 1),
10918                )?;
10919                Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10920            })?;
10921            Ok(Value::Bool(failed.is_none()))
10922        }
10923        "reduce" => {
10924            let items = array_items(recv);
10925            let holes = absent_set(recv);
10926            let cb = arg0(&args);
10927            let acc;
10928            let mut start = 0;
10929            if args.len() >= 2 {
10930                acc = args[1].clone();
10931            } else {
10932                // With no seed the accumulator is the first PRESENT element, so a
10933                // leading run of holes is skipped rather than seeding `undefined`.
10934                match (0..items.len()).find(|i| !holes.contains(i)) {
10935                    Some(i) => {
10936                        acc = items[i].clone();
10937                        start = i + 1;
10938                    }
10939                    None => {
10940                        return Err(host::type_error(
10941                            "Reduce of empty array with no initial value",
10942                        ))
10943                    }
10944                }
10945            }
10946            // Each element is read LIVE at its index, so a callback that
10947            // shrinks the array is observed — the tail is skipped rather than
10948            // folded from a stale snapshot.
10949            let mut cur = acc;
10950            array_walk(recv, |i, it| {
10951                if i < start {
10952                    return Ok(None::<()>);
10953                }
10954                cur = host::invoke(
10955                    &cb,
10956                    vec![
10957                        std::mem::replace(&mut cur, Value::Undef),
10958                        it,
10959                        Value::Float(i as f64),
10960                        this_value.clone(),
10961                    ],
10962                    this_arg(&args, 1),
10963                )?;
10964                Ok(None::<()>)
10965            })?;
10966            Ok(cur)
10967        }
10968        "reduceRight" => {
10969            let cb = arg0(&args);
10970            let n = array_len(recv);
10971            let mut acc;
10972            let mut from = n; // one past the next index to process (walking down)
10973            if args.len() >= 2 {
10974                acc = args[1].clone();
10975            } else {
10976                let holes = absent_set(recv);
10977                match (0..n).rev().find(|i| !holes.contains(i)) {
10978                    Some(k) => {
10979                        acc = get_property(recv, &k.to_string())?;
10980                        from = k;
10981                    }
10982                    None => {
10983                        return Err(host::type_error(
10984                            "Reduce of empty array with no initial value",
10985                        ))
10986                    }
10987                }
10988            }
10989            // `acc` moves into the closure and back out on every step, so it
10990            // lives in an Option the closure can take from and refill.
10991            let mut slot = Some(acc);
10992            array_walk_rev(recv, from, |i, v| {
10993                let prev = slot.take().expect("accumulator is refilled each step");
10994                slot = Some(host::invoke(
10995                    &cb,
10996                    vec![prev, v, Value::Float(i as f64), this_value.clone()],
10997                    None,
10998                )?);
10999                Ok(None::<()>)
11000            })?;
11001            acc = slot.expect("accumulator is refilled each step");
11002            Ok(acc)
11003        }
11004        "findLast" => {
11005            let items = array_items(recv);
11006            let cb = arg0(&args);
11007            for i in (0..items.len()).rev() {
11008                let m = host::invoke(
11009                    &cb,
11010                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11011                    this_arg(&args, 1),
11012                )?;
11013                if with_host(|h| h.truthy(&m)) {
11014                    return Ok(items[i].clone());
11015                }
11016            }
11017            Ok(Value::Undef)
11018        }
11019        "findLastIndex" => {
11020            let items = array_items(recv);
11021            let cb = arg0(&args);
11022            for i in (0..items.len()).rev() {
11023                let m = host::invoke(
11024                    &cb,
11025                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11026                    this_arg(&args, 1),
11027                )?;
11028                if with_host(|h| h.truthy(&m)) {
11029                    return Ok(Value::Float(i as f64));
11030                }
11031            }
11032            Ok(Value::Float(-1.0))
11033        }
11034        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11035        // and the holes are re-created at the tail — `[3,,1].sort()` is
11036        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11037        "sort" => {
11038            let all = array_items(recv);
11039            let holes = absent_set(recv);
11040            let mut items: Vec<Value> = all
11041                .iter()
11042                .enumerate()
11043                .filter(|(i, _)| !holes.contains(i))
11044                .map(|(_, v)| v.clone())
11045                .collect();
11046            sort_values(&mut items, args.first())?;
11047            let present = items.len();
11048            // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11049            // captured at step 1: `Set` for each sorted element, then `Delete`
11050            // for the holes that followed them. Replacing the whole backing
11051            // vector instead discarded anything the COMPARATOR appended —
11052            // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11053            // original length with every pushed element gone.
11054            with_host(|h| {
11055                let len = all.len();
11056                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11057                    if a.len() < len {
11058                        a.resize(len, Value::Undef);
11059                    }
11060                    for (i, v) in items.into_iter().enumerate() {
11061                        a[i] = v;
11062                    }
11063                    for slot in a[present..len].iter_mut() {
11064                        *slot = Value::Undef;
11065                    }
11066                }
11067                h.install_holes(recv, (present..len).collect());
11068            });
11069            Ok(this_value.clone())
11070        }
11071        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11072        "toSorted" => {
11073            let mut items = array_items(recv);
11074            sort_values(&mut items, args.first())?;
11075            Ok(with_host(|h| h.new_array(items)))
11076        }
11077        "toReversed" => {
11078            let mut items = array_items(recv);
11079            items.reverse();
11080            Ok(with_host(|h| h.new_array(items)))
11081        }
11082        "toSpliced" => {
11083            let mut items = array_items(recv);
11084            let len = items.len();
11085            let start = {
11086                let s = arg_num(&args, 0);
11087                if s < 0.0 {
11088                    ((len as f64 + s).max(0.0)) as usize
11089                } else {
11090                    (s as usize).min(len)
11091                }
11092            };
11093            let delete = if args.len() >= 2 {
11094                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11095            } else if args.is_empty() {
11096                0
11097            } else {
11098                len - start
11099            };
11100            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11101            items.splice(start..start + delete, inserts);
11102            Ok(with_host(|h| h.new_array(items)))
11103        }
11104        "with" => {
11105            let mut items = array_items(recv);
11106            let len = items.len() as i64;
11107            let rel = arg_num(&args, 0) as i64;
11108            let idx = if rel < 0 { len + rel } else { rel };
11109            if idx < 0 || idx >= len {
11110                return Err(host::range_error(&format!("Invalid index : {rel}")));
11111            }
11112            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11113            Ok(with_host(|h| h.new_array(items)))
11114        }
11115        "flat" => {
11116            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11117            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11118            let raw = if args.is_empty() {
11119                1.0
11120            } else {
11121                arg_num(&args, 0)
11122            };
11123            let depth = if raw.is_nan() {
11124                0.0
11125            } else if raw.is_infinite() {
11126                raw
11127            } else {
11128                raw.trunc()
11129            };
11130            let mut out = Vec::new();
11131            flatten_into(recv, depth, &mut out)?;
11132            array_species_create(this_value, out)
11133        }
11134        // Live over the array (23.1.5.1): each step reads it as it is then.
11135        "keys" => Ok(array_iterator(recv, host::ArrayIterKind::Keys)),
11136        "values" | "@@iterator" => Ok(array_iterator(recv, host::ArrayIterKind::Values)),
11137        "entries" => Ok(array_iterator(recv, host::ArrayIterKind::Entries)),
11138        "splice" => array_splice(recv, args),
11139        // `Array.prototype.toString` IS `join()` with the default separator
11140        // (23.1.3.36), so it converts each element with `ToString` too — and
11141        // shares its cycle cut, which is the whole reason it must not call
11142        // `join_parts` directly: `ToString` of a nested array lands back here.
11143        "toString" => join_array(recv, ","),
11144        // An Array inherits from `Object.prototype` too, so the methods it does
11145        // not override resolve there. `[].hasOwnProperty` already read back as a
11146        // function through the property path, but CALLING it landed here and
11147        // threw `is not a function`.
11148        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11149        _ => Err(host::type_error(&format!("{name} is not a function"))),
11150    }
11151}
11152
11153/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11154/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11155/// cut, which is not optional here: `ToString` of an element that is itself an
11156/// array re-enters through `toString`, so guarding only `join` left
11157/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11158/// process. On node v26.7.0 that expression is `""`.
11159fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11160    if !host::join_stack_push(recv) {
11161        return Ok(with_host(|h| h.new_str(String::new())));
11162    }
11163    // 23.1.3.18 step 6: the length is captured once, then each element is read
11164    // and STRINGIFIED before the next is read. Both halves are observable —
11165    // a getter or a `toString` that shrinks the array is seen by every later
11166    // element, which a read-all-then-convert pass misses.
11167    let parts = (|| -> Result<Vec<String>, String> {
11168        let len = array_len(recv);
11169        let mut out = Vec::with_capacity(len);
11170        for i in 0..len {
11171            let v = array_elem_live(recv, i)?;
11172            out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11173        }
11174        Ok(out)
11175    })();
11176    host::join_stack_pop();
11177    let s = parts?.join(sep);
11178    Ok(with_host(|h| h.new_str(s)))
11179}
11180
11181/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11182/// `null`/`undefined` element contributes the empty string, every other element
11183/// is `ToString(element)` — which for an object means invoking its `toString`,
11184/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11185/// `"[object Object]"`.
11186///
11187/// The all-primitive array — the overwhelmingly common one — is rendered under
11188/// a single host borrow; only an array actually holding an object pays for the
11189/// re-entrant per-element conversion.
11190fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11191    let fast = with_host(|h| {
11192        items
11193            .iter()
11194            .map(|x| match x {
11195                Value::Undef => Some(String::new()),
11196                _ if h.is_null(x) => Some(String::new()),
11197                // A SYMBOL element is primitive but has no `ToString`, so it must
11198                // fall through to the fallible path and throw there:
11199                // `[Symbol()].join()` is a TypeError on node v26.7.0.
11200                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11201                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11202                _ => None,
11203            })
11204            .collect::<Vec<_>>()
11205    });
11206    if fast.iter().all(Option::is_some) {
11207        return Ok(fast.into_iter().flatten().collect());
11208    }
11209    let mut out = Vec::with_capacity(items.len());
11210    for (x, p) in items.iter().zip(fast) {
11211        match p {
11212            Some(s) => out.push(s),
11213            None => {
11214                let s = host::to_string_value(x)?;
11215                out.push(with_host(|h| h.str_of(&s)));
11216            }
11217        }
11218    }
11219    Ok(out)
11220}
11221
11222/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11223/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11224/// the merge step; default order is by the string form of each element.
11225/// Propagates a comparator error.
11226///
11227/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11228/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11229/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11230/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11231/// number of times it is called is.
11232pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11233    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11234    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11235    // invoke path and reporting the generic `null is not a function`.
11236    let cmp = match cmp {
11237        Some(Value::Undef) => None,
11238        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11239            // V8 renders the offending value with `NoSideEffectsToString`, not
11240            // with `util.inspect`: a string appears bare (`: x`) rather than
11241            // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11242            let shown = no_side_effects_string(v);
11243            return Err(host::type_error(&format!(
11244                "The comparison function must be either a function or undefined: {shown}"
11245            )));
11246        }
11247        other => other,
11248    };
11249    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11250    // comparator — it sorts to the end after the defined values are ordered.
11251    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11252    // node v26.7.0; the insertion sort called the comparator twice, on
11253    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11254    // is `undefined`, so swapping keeps the defined values in input order.
11255    let mut defined = 0;
11256    for i in 0..items.len() {
11257        if !matches!(items[i], Value::Undef) {
11258            items.swap(defined, i);
11259            defined += 1;
11260        }
11261    }
11262    merge_sort(&mut items[..defined], cmp)
11263}
11264
11265/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11266/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11267/// `> 0` and the pair keeps its input order.
11268fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11269    match cmp {
11270        Some(cb) => {
11271            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11272            Ok(with_host(|h| h.to_number(&v)))
11273        }
11274        None => {
11275            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11276            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11277            // from Rust's `String` order off the BMP.
11278            let x = with_host(|h| h.str_of(a));
11279            let y = with_host(|h| h.str_of(b));
11280            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11281                Ok(1.0)
11282            } else {
11283                Ok(-1.0)
11284            }
11285        }
11286    }
11287}
11288
11289/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11290/// array cannot walk the native stack the JS comparator also runs on, and the
11291/// two buffers are swapped each pass instead of copied back.
11292fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11293    let n = items.len();
11294    if n < 2 {
11295        return Ok(());
11296    }
11297    let mut src = items.to_vec();
11298    let mut dst = src.clone();
11299    let mut width = 1;
11300    while width < n {
11301        let mut lo = 0;
11302        while lo < n {
11303            let mid = (lo + width).min(n);
11304            let hi = (lo + 2 * width).min(n);
11305            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11306            lo = hi;
11307        }
11308        std::mem::swap(&mut src, &mut dst);
11309        width *= 2;
11310    }
11311    items.clone_from_slice(&src);
11312    Ok(())
11313}
11314
11315/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11316/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11317/// keeps the two `k:0` entries in input order, as node does.
11318fn merge(
11319    left: &[Value],
11320    right: &[Value],
11321    out: &mut [Value],
11322    cmp: Option<&Value>,
11323) -> Result<(), String> {
11324    let (mut i, mut j, mut k) = (0, 0, 0);
11325    while i < left.len() && j < right.len() {
11326        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11327            out[k] = right[j].clone();
11328            j += 1;
11329        } else {
11330            out[k] = left[i].clone();
11331            i += 1;
11332        }
11333        k += 1;
11334    }
11335    for v in left[i..].iter().chain(&right[j..]) {
11336        out[k] = v.clone();
11337        k += 1;
11338    }
11339    Ok(())
11340}
11341
11342/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11343/// f64 so `Infinity` (full flatten) and finite counts share one path.
11344///
11345/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11346/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11347/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11348/// stack floor the VM does, so the answer is a catchable error rather than the
11349/// `fatal runtime error: stack overflow` abort this used to produce.
11350/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11351/// elements because each level tests `HasProperty` before recursing, so a hole
11352/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11353fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11354    if host::stack_exhausted() {
11355        return Err(host::stack_overflow_error());
11356    }
11357    let items = array_items(src);
11358    let holes = absent_set(src);
11359    for (i, it) in items.into_iter().enumerate() {
11360        if holes.contains(&i) {
11361            continue;
11362        }
11363        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11364        if nested {
11365            flatten_into(&it, depth - 1.0, out)?;
11366        } else {
11367            out.push(it);
11368        }
11369    }
11370    Ok(())
11371}
11372
11373fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11374    let len = array_len(recv);
11375    let start = {
11376        let s = arg_num(&args, 0);
11377        if s < 0.0 {
11378            ((len as f64 + s).max(0.0)) as usize
11379        } else {
11380            (s as usize).min(len)
11381        }
11382    };
11383    let delete = if args.len() >= 2 {
11384        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11385    } else {
11386        len - start
11387    };
11388    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11389    let inserted = inserts.len();
11390    // The receiver's holes shift by (inserted - deleted) past the cut, and the
11391    // ones inside the cut move into the RETURNED array at their offset there.
11392    let holes = hole_set(recv);
11393    let removed = with_host(|h| {
11394        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11395            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11396            removed
11397        } else {
11398            Vec::new()
11399        }
11400    });
11401    let spliced = with_host(|h| {
11402        h.install_holes(
11403            recv,
11404            holes
11405                .iter()
11406                .filter_map(|&i| {
11407                    if i < start {
11408                        Some(i)
11409                    } else if i < start + delete {
11410                        None
11411                    } else {
11412                        Some(i - delete + inserted)
11413                    }
11414                })
11415                .collect(),
11416        );
11417        (removed, holes.clone())
11418    });
11419    // The REMOVED elements come back as an array of the receiver's species
11420    // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11421    let (removed, holes) = spliced;
11422    let out = array_species_create(recv, removed)?;
11423    with_host(|h| {
11424        h.install_holes(
11425            &out,
11426            holes
11427                .iter()
11428                .filter(|&&i| i >= start && i < start + delete)
11429                .map(|&i| i - start)
11430                .collect(),
11431        );
11432    });
11433    Ok(out)
11434}
11435
11436fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11437    let norm = |v: f64| -> usize {
11438        if v < 0.0 {
11439            ((len as f64 + v).max(0.0)) as usize
11440        } else {
11441            (v as usize).min(len)
11442        }
11443    };
11444    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11445        0
11446    } else {
11447        norm(arg_num(args, 0))
11448    };
11449    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11450        len
11451    } else {
11452        norm(arg_num(args, 1))
11453    };
11454    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11455    // never a reversed one: JS `slice` clamps `end` up to `start`.
11456    (lo, hi.max(lo))
11457}
11458
11459/// The argument positions each `String.prototype` method coerces with
11460/// `ToNumber` rather than `ToString`. Everything not listed is a string
11461/// position — which matters only for a SYMBOL argument, the one value both
11462/// conversions refuse, and refuse with different wording.
11463///
11464/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11465/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11466/// pair the other way round (a length then a pad string).
11467const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11468    ("at", &[0]),
11469    ("charAt", &[0]),
11470    ("charCodeAt", &[0]),
11471    ("codePointAt", &[0]),
11472    ("endsWith", &[1]),
11473    ("includes", &[1]),
11474    ("indexOf", &[1]),
11475    ("lastIndexOf", &[1]),
11476    ("padEnd", &[0]),
11477    ("padStart", &[0]),
11478    ("repeat", &[0]),
11479    ("slice", &[0, 1]),
11480    ("split", &[1]),
11481    ("startsWith", &[1]),
11482    ("substr", &[0, 1]),
11483    ("substring", &[0, 1]),
11484];
11485
11486/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11487/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11488/// rendered `Symbol(d)` into the result — silently, which is the shape of
11489/// mistake that makes a symbol key leak into text.
11490fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11491    let numeric = STRING_METHOD_NUMERIC_ARGS
11492        .iter()
11493        .find(|(m, _)| *m == name)
11494        .map(|(_, ps)| *ps)
11495        .unwrap_or(&[]);
11496    for (i, a) in args.iter().enumerate() {
11497        if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11498            let kind = if numeric.contains(&i) {
11499                "number"
11500            } else {
11501                "string"
11502            };
11503            return Err(host::type_error(&format!(
11504                "Cannot convert a Symbol value to a {kind}"
11505            )));
11506        }
11507    }
11508    Ok(())
11509}
11510
11511/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11512/// reads them: a numeric position through `ToNumber`, every other through
11513/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11514/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11515/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11516///
11517/// The positions that must NOT be coerced are the ones with their own protocol:
11518/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11519/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11520/// `replace`/`replaceAll`. Each of those already has a path that handles the
11521/// value as an object, and stringifying it first would take that path away.
11522/// The argument positions each `Array.prototype` method coerces with
11523/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11524/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11525/// are stored as given, and `indexOf`/`includes` compare their first argument
11526/// without converting it.
11527const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11528    ("at", &[0]),
11529    ("copyWithin", &[0, 1, 2]),
11530    ("fill", &[1, 2]),
11531    ("flat", &[0]),
11532    ("includes", &[1]),
11533    ("indexOf", &[1]),
11534    ("lastIndexOf", &[1]),
11535    ("slice", &[0, 1]),
11536    ("splice", &[0, 1]),
11537    ("toSpliced", &[0, 1]),
11538    ("with", &[0]),
11539];
11540
11541/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11542/// number, and is deliberately absent.
11543const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11544    ("toExponential", &[0]),
11545    ("toFixed", &[0]),
11546    ("toPrecision", &[0]),
11547    ("toString", &[0]),
11548];
11549
11550/// Replace the listed argument positions with their `ToNumber` value, running a
11551/// user `valueOf` and propagating a throw from it. Every one of these read the
11552/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11553/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11554/// was a RangeError.
11555/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11556fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11557    let v = args.get(i).cloned().unwrap_or(Value::Undef);
11558    let p = host::to_primitive(&v, "number")?;
11559    Ok(with_host(|h| h.to_number(&p)))
11560}
11561
11562fn coerce_numeric_args(
11563    table: &[(&str, &[usize])],
11564    name: &str,
11565    mut args: Vec<Value>,
11566) -> Result<Vec<Value>, String> {
11567    let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11568        return Ok(args);
11569    };
11570    for &i in *positions {
11571        let Some(a) = args.get(i) else { continue };
11572        if matches!(a, Value::Undef) {
11573            continue;
11574        }
11575        let p = host::to_primitive(a, "number")?;
11576        args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11577    }
11578    Ok(args)
11579}
11580
11581/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11582/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11583/// matches at position 0.
11584fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11585    let src = if matches!(v, Value::Undef) {
11586        String::new()
11587    } else {
11588        with_host(|h| h.str_of(v))
11589    };
11590    let fv = with_host(|h| h.new_str(flags.to_string()));
11591    let sv = with_host(|h| h.new_str(src));
11592    regexp_ctor(&[sv, fv])
11593}
11594
11595fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11596    let numeric = STRING_METHOD_NUMERIC_ARGS
11597        .iter()
11598        .find(|(m, _)| *m == name)
11599        .map(|(_, ps)| *ps)
11600        .unwrap_or(&[]);
11601    let protocol = match name {
11602        "replace" | "replaceAll" => Some("@@replace"),
11603        "split" => Some("@@split"),
11604        "match" => Some("@@match"),
11605        "matchAll" => Some("@@matchAll"),
11606        "search" => Some("@@search"),
11607        // These three do not CONSUME `Symbol.match`, they reject a value that
11608        // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11609        // the object intact so that check still sees one; stringifying first
11610        // turned the TypeError into an ordinary search.
11611        "startsWith" | "endsWith" | "includes" => Some("@@match"),
11612        _ => None,
11613    };
11614    let mut out = Vec::with_capacity(args.len());
11615    for (i, a) in args.into_iter().enumerate() {
11616        if matches!(a, Value::Undef) {
11617            out.push(a);
11618            continue;
11619        }
11620        if numeric.contains(&i) {
11621            let p = host::to_primitive(&a, "number")?;
11622            out.push(Value::Float(with_host(|h| h.to_number(&p))));
11623            continue;
11624        }
11625        // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11626        // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11627        // regexp and coerces like anything else. The consuming protocols use
11628        // `GetMethod`, which additionally requires a callable.
11629        let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11630        let carries = |p: &str| match host::protocol_lookup(&a, p) {
11631            Ok(Some(m)) => {
11632                if is_regexp_like {
11633                    with_host(|h| h.truthy(&m))
11634                } else {
11635                    with_host(|h| host::is_callable(h, &m))
11636                }
11637            }
11638            _ => false,
11639        };
11640        let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11641            || (i == 0 && protocol.is_some_and(carries))
11642            || (i == 1
11643                && matches!(name, "replace" | "replaceAll")
11644                && with_host(|h| host::is_callable(h, &a)));
11645        if exempt {
11646            out.push(a);
11647            continue;
11648        }
11649        out.push(host::to_string_value(&a)?);
11650    }
11651    Ok(out)
11652}
11653
11654fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11655    reject_symbol_args(name, &args)?;
11656    let args = coerce_string_args(name, args)?;
11657    // Every index-bearing method below counts UTF-16 code units, so they all
11658    // work off this one decoding rather than off `s.chars()` (code points),
11659    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11660    let u = crate::utf16::Units::of(s);
11661    match name {
11662        // `for…of` / spread over a string iterates CODE POINTS, not code units:
11663        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11664        // is the one string operation that is specified in chars, so it stays
11665        // on `s.chars()` on purpose — do not "fix" it to match the others.
11666        "@@iterator" => {
11667            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11668            Ok(with_host(|h| {
11669                h.alloc(JsObj::Iter {
11670                    items,
11671                    idx: 0,
11672                    array: None,
11673                })
11674            }))
11675        }
11676        "toUpperCase" => Ok(new_s(s.to_uppercase())),
11677        "toLowerCase" => Ok(new_s(s.to_lowercase())),
11678        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11679        // from the plain forms only for the locale-specific mappings (Turkish
11680        // dotless i, Lithuanian accents); with no locale argument they are the
11681        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11682        // `to_lowercase`. They threw `is not a function` before, so the common
11683        // no-argument call — the only form this runtime can answer, since it
11684        // carries no ICU — failed outright rather than agreeing with node.
11685        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11686        // is `'i'` here and `'ı'` in node.
11687        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11688        // has no locale rendering. Missing it made an ARRAY of strings fail too,
11689        // since `Array.prototype.toLocaleString` invokes it per element.
11690        "toLocaleString" => Ok(new_s(s.to_string())),
11691        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11692        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11693        // Locale comparison (ASCII approximation of ICU collation): primary by
11694        // case-folded order, then lowercase sorts before uppercase at a tie.
11695        "localeCompare" => {
11696            let other = with_host(|h| h.str_of(&arg0(&args)));
11697            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11698            let r = match la.cmp(&lb) {
11699                std::cmp::Ordering::Less => -1.0,
11700                std::cmp::Ordering::Greater => 1.0,
11701                std::cmp::Ordering::Equal => {
11702                    let mut t = 0.0;
11703                    for (ca, cb) in s.chars().zip(other.chars()) {
11704                        if ca != cb {
11705                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11706                            break;
11707                        }
11708                    }
11709                    t
11710                }
11711            };
11712            Ok(Value::Float(r))
11713        }
11714        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11715        //
11716        // This used to return the receiver unchanged and only validate the FORM
11717        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11718        // one code point) and `"Å"` (NFD, two) stayed distinct under
11719        // `.normalize()`, so the standard way to compare Unicode text for
11720        // canonical equivalence silently answered `false`, and `NFKC` never
11721        // folded a compatibility character (`"fi"` stayed one code point instead
11722        // of becoming `"fi"`). The tables come from `unicode-normalization`.
11723        "normalize" => {
11724            use unicode_normalization::UnicodeNormalization;
11725            let form = match args.first() {
11726                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11727                _ => "NFC".to_string(),
11728            };
11729            let out = match form.as_str() {
11730                "NFC" => s.nfc().collect::<String>(),
11731                "NFD" => s.nfd().collect::<String>(),
11732                "NFKC" => s.nfkc().collect::<String>(),
11733                "NFKD" => s.nfkd().collect::<String>(),
11734                _ => {
11735                    return Err(host::range_error(
11736                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11737                    ))
11738                }
11739            };
11740            Ok(new_s(out))
11741        }
11742        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11743        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11744        // value this runtime can hold is well-formed by construction and
11745        // `toWellFormed` has nothing to replace. Both answers are therefore
11746        // exact for every string that survives storage; the one case node
11747        // answers differently is a surrogate half extracted by `charAt`/`slice`,
11748        // which is already `U+FFFD` here — the documented lone-surrogate
11749        // boundary in `utf16`, not a separate gap.
11750        "isWellFormed" => Ok(Value::Bool(true)),
11751        "toWellFormed" => Ok(new_s(s.to_string())),
11752        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11753        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11754        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11755        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11756        "toString" | "valueOf" => Ok(new_s(s.to_string())),
11757        "charAt" => {
11758            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11759            Ok(new_s(at.unwrap_or_default()))
11760        }
11761        "at" => {
11762            let n = arg_num(&args, 0);
11763            // A negative position counts back from the end; `NaN` is 0. An
11764            // infinite position is out of range in either direction.
11765            let i = if n.is_nan() {
11766                Some(0i64)
11767            } else if n.is_finite() {
11768                let i = n.trunc() as i64;
11769                Some(if i < 0 { i + u.len() as i64 } else { i })
11770            } else {
11771                None
11772            };
11773            match i
11774                .and_then(|i| usize::try_from(i).ok())
11775                .and_then(|i| u.unit_str(i))
11776            {
11777                Some(c) => Ok(new_s(c)),
11778                None => Ok(Value::Undef),
11779            }
11780        }
11781        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11782        // astral character, not the character. `codePointAt` looks ahead one
11783        // unit and reports the whole scalar when the pair is well formed. They
11784        // agree everywhere on the BMP, which is why they used to share an arm.
11785        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11786        // `codePointAt` yields `undefined` (measured on node v26.7.0).
11787        "charCodeAt" => {
11788            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11789            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11790        }
11791        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11792            Some(cp) => Ok(Value::Float(f64::from(cp))),
11793            None => Ok(Value::Undef),
11794        },
11795        // The search quartet all honor their optional position argument.
11796        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11797        // parameterCount walks a query string with exactly that call.
11798        "indexOf" => {
11799            let needle = needle_units(&args);
11800            let from = clamp_pos(arg_num(&args, 1), u.len());
11801            Ok(Value::Float(
11802                search_from(u.as_slice(), needle.as_slice(), from)
11803                    .map(|i| i as f64)
11804                    .unwrap_or(-1.0),
11805            ))
11806        }
11807        "lastIndexOf" => {
11808            let needle = needle_units(&args);
11809            // An absent or NaN position means "search the whole string".
11810            let n = arg_num(&args, 1);
11811            let upto = if n.is_nan() {
11812                u.len()
11813            } else {
11814                clamp_pos(n, u.len())
11815            };
11816            Ok(Value::Float(
11817                search_last(u.as_slice(), needle.as_slice(), upto)
11818                    .map(|i| i as f64)
11819                    .unwrap_or(-1.0),
11820            ))
11821        }
11822        // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11823        // argument outright, and `IsRegExp` is what decides — so an object
11824        // advertising `Symbol.match` is rejected too. None of them checked.
11825        "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11826            Err(host::type_error(&format!(
11827                "First argument to String.prototype.{name} must not be a regular expression"
11828            )))
11829        }
11830        "includes" => {
11831            let needle = needle_units(&args);
11832            let from = clamp_pos(arg_num(&args, 1), u.len());
11833            Ok(Value::Bool(
11834                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11835            ))
11836        }
11837        "startsWith" => {
11838            let needle = needle_units(&args);
11839            let from = clamp_pos(arg_num(&args, 1), u.len());
11840            Ok(Value::Bool(
11841                u.as_slice()[from..].starts_with(needle.as_slice()),
11842            ))
11843        }
11844        "endsWith" => {
11845            let needle = needle_units(&args);
11846            // The 2nd argument is where the string is treated as ENDING.
11847            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11848                u.len()
11849            } else {
11850                clamp_pos(arg_num(&args, 1), u.len())
11851            };
11852            Ok(Value::Bool(
11853                u.as_slice()[..end].ends_with(needle.as_slice()),
11854            ))
11855        }
11856        "slice" => {
11857            let (lo, hi) = slice_bounds(&args, u.len());
11858            Ok(new_s(u.slice(lo, hi)))
11859        }
11860        "substring" => {
11861            let mut a = arg_num(&args, 0).max(0.0) as usize;
11862            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11863                u.len()
11864            } else {
11865                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11866            };
11867            a = a.min(u.len());
11868            if a > b {
11869                std::mem::swap(&mut a, &mut b);
11870            }
11871            Ok(new_s(u.slice(a, b)))
11872        }
11873        "substr" => {
11874            // A negative start counts from the end: max(len + start, 0).
11875            let len = u.len() as i64;
11876            let mut start = arg_num(&args, 0) as i64;
11877            if start < 0 {
11878                start = (len + start).max(0);
11879            }
11880            let start = (start as usize).min(u.len());
11881            let count = if args.len() >= 2 {
11882                arg_num(&args, 1).max(0.0) as usize
11883            } else {
11884                u.len()
11885            };
11886            let end = start.saturating_add(count).min(u.len());
11887            Ok(new_s(u.slice(start, end)))
11888        }
11889        "repeat" => {
11890            let n = arg_num(&args, 0);
11891            // `RangeError`, not `TypeError`, and the count is named:
11892            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11893            if n < 0.0 || !n.is_finite() {
11894                return Err(host::range_error(&format!(
11895                    "Invalid count value: {}",
11896                    host::fmt_number(n)
11897                )));
11898            }
11899            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11900            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11901            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11902            // is `RangeError: Invalid string length`.
11903            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11904                return Err(host::invalid_string_length());
11905            }
11906            Ok(new_s(s.repeat(n as usize)))
11907        }
11908        "concat" => {
11909            let mut out = s.to_string();
11910            for a in &args {
11911                out.push_str(&with_host(|h| h.str_of(a)));
11912            }
11913            Ok(new_s(out))
11914        }
11915        "padStart" => Ok(new_s(pad(s, &args, true)?)),
11916        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11917        // Regex-taking string methods: dispatch to the regexp module when the
11918        // argument is a RegExp; otherwise keep the plain-string behavior.
11919        // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11920        // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11921        // though a RegExp does define that method. Delegating first skipped the
11922        // check and silently did a single replacement.
11923        "replaceAll"
11924            if is_regexp_arg(&arg0(&args))
11925                && !with_host(
11926                    |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11927                ) =>
11928        {
11929            Err(host::type_error(
11930                "String.prototype.replaceAll called with a non-global RegExp argument",
11931            ))
11932        }
11933        "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11934            if symbol_protocol(
11935                &arg0(&args),
11936                match name {
11937                    "match" => "@@match",
11938                    "matchAll" => "@@matchAll",
11939                    "search" => "@@search",
11940                    "split" => "@@split",
11941                    _ => "@@replace",
11942                },
11943            )
11944            .is_some() =>
11945        {
11946            let sym = match name {
11947                "match" => "@@match",
11948                "matchAll" => "@@matchAll",
11949                "search" => "@@search",
11950                "split" => "@@split",
11951                _ => "@@replace",
11952            };
11953            let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11954            let sv = with_host(|h| h.new_str(s.to_string()));
11955            let mut rest = vec![sv];
11956            rest.extend(args.iter().skip(1).cloned());
11957            host::invoke(&f, rest, Some(arg0(&args)))
11958        }
11959        // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11960        // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11961        // answered `null` for every string argument, which reads as "no match"
11962        // — the one answer a caller cannot tell from a real failure.
11963        // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11964        "match" => {
11965            let a = arg0(&args);
11966            let re = if is_regexp_arg(&a) {
11967                a
11968            } else {
11969                regexp_from_arg(&a, "")?
11970            };
11971            crate::regexp::str_match(s, &re)
11972        }
11973        "matchAll" => {
11974            let a = arg0(&args);
11975            let re = if is_regexp_arg(&a) {
11976                a
11977            } else {
11978                regexp_from_arg(&a, "g")?
11979            };
11980            crate::regexp::str_match_all(s, &re)
11981        }
11982        "search" => {
11983            if is_regexp_arg(&arg0(&args)) {
11984                crate::regexp::str_search(s, &arg0(&args))
11985            } else {
11986                // 22.1.3.17 builds a RegExp from the argument, so a
11987                // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11988                // 1. The substring approximation this replaces agreed only for
11989                // a literal needle, and answered -1 for an absent argument
11990                // where the empty pattern matches at 0.
11991                let re = regexp_from_arg(&arg0(&args), "")?;
11992                crate::regexp::str_search(s, &re)
11993            }
11994        }
11995        "replace" => {
11996            let pat = arg0(&args);
11997            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11998            if is_regexp_arg(&pat) {
11999                crate::regexp::str_replace_regex(s, &pat, &repl, false)
12000            } else if with_host(|h| host::is_callable(h, &repl)) {
12001                Ok(new_s(replace_str_fn(
12002                    s,
12003                    &with_host(|h| h.str_of(&pat)),
12004                    &repl,
12005                    false,
12006                )?))
12007            } else {
12008                let from = with_host(|h| h.str_of(&pat));
12009                let to = with_host(|h| h.str_of(&repl));
12010                Ok(new_s(replace_str_plain(s, &from, &to, false)))
12011            }
12012        }
12013        "replaceAll" => {
12014            let pat = arg0(&args);
12015            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12016            if is_regexp_arg(&pat) {
12017                // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12018                // because `replaceAll` cannot honour "all" without `g`. This
12019                // used to replace only the first match and say nothing.
12020                let global = with_host(|h| match h.get(&pat) {
12021                    Some(JsObj::RegExp(r)) => r.global,
12022                    _ => true,
12023                });
12024                if !global {
12025                    return Err(host::type_error(
12026                        "String.prototype.replaceAll called with a non-global RegExp argument",
12027                    ));
12028                }
12029                crate::regexp::str_replace_regex(s, &pat, &repl, true)
12030            } else if with_host(|h| host::is_callable(h, &repl)) {
12031                Ok(new_s(replace_str_fn(
12032                    s,
12033                    &with_host(|h| h.str_of(&pat)),
12034                    &repl,
12035                    true,
12036                )?))
12037            } else {
12038                let from = with_host(|h| h.str_of(&pat));
12039                let to = with_host(|h| h.str_of(&repl));
12040                Ok(new_s(replace_str_plain(s, &from, &to, true)))
12041            }
12042        }
12043        "split" => {
12044            if is_regexp_arg(&arg0(&args)) {
12045                let limit = args
12046                    .get(1)
12047                    .filter(|v| !matches!(v, Value::Undef))
12048                    .map(|v| with_host(|h| h.to_number(v)) as usize);
12049                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12050            }
12051            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12052                vec![new_s(s.to_string())]
12053            } else {
12054                let sep = with_host(|h| h.str_of(&args[0]));
12055                if sep.is_empty() {
12056                    // `split('')` yields one element per code UNIT, so an astral
12057                    // character becomes its two surrogate halves.
12058                    (0..u.len())
12059                        .filter_map(|i| u.unit_str(i))
12060                        .map(new_s)
12061                        .collect()
12062                } else {
12063                    s.split(&sep as &str)
12064                        .map(|p| new_s(p.to_string()))
12065                        .collect()
12066                }
12067            };
12068            // Optional limit: keep at most `limit` substrings.
12069            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12070                let n = with_host(|h| h.to_number(lim));
12071                if n.is_finite() && n >= 0.0 {
12072                    parts.truncate(n as usize);
12073                }
12074            }
12075            Ok(with_host(|h| h.new_array(parts)))
12076        }
12077        _ => Err(host::type_error(&format!("{name} is not a function"))),
12078    }
12079}
12080
12081/// GetSubstitution (22.1.3.19) for a STRING search value.
12082///
12083/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12084/// the pattern is a regexp or a plain string, but the string path here did a
12085/// raw `str::replace` and passed the template through verbatim — so
12086/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12087/// regexp path has always expanded them.
12088///
12089/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12090/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12091/// what node does.
12092fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12093    let chars: Vec<char> = templ.chars().collect();
12094    let mut out = String::new();
12095    let mut i = 0;
12096    while i < chars.len() {
12097        if chars[i] == '$' && i + 1 < chars.len() {
12098            match chars[i + 1] {
12099                '$' => {
12100                    out.push('$');
12101                    i += 2;
12102                    continue;
12103                }
12104                '&' => {
12105                    out.push_str(matched);
12106                    i += 2;
12107                    continue;
12108                }
12109                '`' => {
12110                    out.push_str(&subject[..position]);
12111                    i += 2;
12112                    continue;
12113                }
12114                '\'' => {
12115                    out.push_str(&subject[position + matched.len()..]);
12116                    i += 2;
12117                    continue;
12118                }
12119                _ => {}
12120            }
12121        }
12122        out.push(chars[i]);
12123        i += 1;
12124    }
12125    out
12126}
12127
12128/// `replace`/`replaceAll` with a string pattern and a string replacement,
12129/// expanding each match's `$` patterns against its own position.
12130fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12131    if from.is_empty() && !all {
12132        return format!("{}{s}", substitute_plain(to, "", 0, s));
12133    }
12134    let mut out = String::new();
12135    let mut rest = 0usize;
12136    while let Some(rel) = s[rest..].find(from) {
12137        let at = rest + rel;
12138        out.push_str(&s[rest..at]);
12139        out.push_str(&substitute_plain(to, from, at, s));
12140        rest = at + from.len();
12141        if !all {
12142            break;
12143        }
12144        // An empty pattern matches between every character; step one along so
12145        // the scan terminates.
12146        if from.is_empty() {
12147            if rest >= s.len() {
12148                break;
12149            }
12150            let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12151            out.push_str(&s[rest..rest + step]);
12152            rest += step;
12153        }
12154    }
12155    out.push_str(&s[rest..]);
12156    out
12157}
12158
12159fn new_s(s: String) -> Value {
12160    with_host(|h| h.new_str(s))
12161}
12162
12163/// Where a forward `indexOf`/`includes` search starts, given the optional
12164/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12165/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12166/// or past the end finds nothing, which callers report as `-1` / `false`.
12167pub(crate) fn search_start(n: f64, len: usize) -> usize {
12168    if n.is_nan() {
12169        return 0;
12170    }
12171    let n = n.trunc();
12172    if n >= 0.0 {
12173        if n >= len as f64 {
12174            len
12175        } else {
12176            n as usize
12177        }
12178    } else {
12179        let from_end = len as f64 + n;
12180        if from_end <= 0.0 {
12181            0
12182        } else {
12183            from_end as usize
12184        }
12185    }
12186}
12187
12188/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12189/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12190/// the last element — which is why this takes an `Option` rather than reading
12191/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12192/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12193pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12194    if len == 0 {
12195        return None;
12196    }
12197    let n = match from {
12198        None => return Some(len - 1),
12199        Some(v) if v.is_nan() => 0.0,
12200        Some(v) => v.trunc(),
12201    };
12202    if n >= 0.0 {
12203        Some(if n >= len as f64 { len - 1 } else { n as usize })
12204    } else {
12205        let k = len as f64 + n;
12206        if k < 0.0 {
12207            None
12208        } else {
12209            Some(k as usize)
12210        }
12211    }
12212}
12213
12214/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12215/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12216fn clamp_pos(n: f64, len: usize) -> usize {
12217    if n.is_nan() || n <= 0.0 {
12218        0
12219    } else if n >= len as f64 {
12220        len
12221    } else {
12222        n.trunc() as usize
12223    }
12224}
12225
12226/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12227/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12228/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12229fn unit_pos(n: f64) -> Option<usize> {
12230    if n.is_nan() {
12231        Some(0)
12232    } else if n < 0.0 || !n.is_finite() {
12233        None
12234    } else {
12235        Some(n.trunc() as usize)
12236    }
12237}
12238
12239/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12240/// the needle is compared in the same alphabet the haystack is indexed by.
12241fn needle_units(args: &[Value]) -> crate::utf16::Units {
12242    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12243}
12244
12245/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12246/// needle matches at `from` itself, as JS specifies.
12247fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12248    if needle.is_empty() {
12249        return Some(from.min(hay.len()));
12250    }
12251    if needle.len() > hay.len() {
12252        return None;
12253    }
12254    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12255}
12256
12257/// The highest index `<= upto` at which `needle` occurs in `hay`.
12258fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12259    if needle.is_empty() {
12260        return Some(upto.min(hay.len()));
12261    }
12262    if needle.len() > hay.len() {
12263        return None;
12264    }
12265    let last = hay.len() - needle.len();
12266    (0..=upto.min(last))
12267        .rev()
12268        .find(|&i| &hay[i..i + needle.len()] == needle)
12269}
12270
12271fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12272    let target_f = arg_num(args, 0);
12273    let target = if target_f.is_finite() && target_f > 0.0 {
12274        target_f as usize
12275    } else {
12276        0
12277    };
12278    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12279    // is `'-𝒳'` in node, not `'--𝒳'`.
12280    let cur = crate::utf16::len(s);
12281    if cur >= target {
12282        return Ok(s.to_string());
12283    }
12284    let filler = if args.len() >= 2 {
12285        with_host(|h| h.str_of(&args[1]))
12286    } else {
12287        " ".to_string()
12288    };
12289    if filler.is_empty() {
12290        return Ok(s.to_string());
12291    }
12292    // Checked only AFTER the two short-circuits, which is the order V8 uses:
12293    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12294    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12295    if target_f > host::MAX_STRING_LENGTH as f64 {
12296        return Err(host::invalid_string_length());
12297    }
12298    let need = target - cur;
12299    let fill = crate::utf16::Units::of(&filler);
12300    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12301    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12302    // (see src/utf16.rs).
12303    let units: Vec<u16> = (0..need)
12304        .filter_map(|i| fill.unit(i % fill.len()))
12305        .collect();
12306    let padding = crate::utf16::to_string_lossy(&units);
12307    Ok(if start {
12308        format!("{padding}{s}")
12309    } else {
12310        format!("{s}{padding}")
12311    })
12312}
12313
12314/// V8's radix rejection, shared by `Number.prototype.toString` and
12315/// `BigInt.prototype.toString` — one string, because they are one message and
12316/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12317/// must be").
12318const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12319
12320/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12321fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12322    match name {
12323        "toString" => {
12324            let radix = match args.first() {
12325                None | Some(Value::Undef) => 10,
12326                Some(_) => {
12327                    let t = arg_num(&args, 0).trunc();
12328                    if !(2.0..=36.0).contains(&t) {
12329                        return Err(host::range_error(RADIX_RANGE));
12330                    }
12331                    t as u32
12332                }
12333            };
12334            Ok(new_s(b.to_str_radix(radix)))
12335        }
12336        // `BigInt.prototype.toLocaleString` groups thousands like the Number
12337        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12338        // returning the bare digits made it the only numeric type that skipped
12339        // grouping. Same en-US-shaped output as `Number.prototype`; the
12340        // `locales`/`options` arguments are ignored (no ICU here).
12341        "toLocaleString" => {
12342            let digits = b.magnitude().to_string();
12343            let sign = if b.sign() == num_bigint::Sign::Minus {
12344                "-"
12345            } else {
12346                ""
12347            };
12348            Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12349        }
12350        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12351        _ => Err(host::type_error(&format!("{name} is not a function"))),
12352    }
12353}
12354
12355fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12356    let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12357    match name {
12358        "toFixed" => {
12359            let digits = arg_num(&args, 0);
12360            if !(0.0..=100.0).contains(&digits.trunc()) {
12361                return Err(host::range_error(
12362                    "toFixed() digits argument must be between 0 and 100",
12363                ));
12364            }
12365            Ok(new_s(to_fixed(n, digits as usize)))
12366        }
12367        "toExponential" => {
12368            // `undefined` (or a missing argument) selects the shortest form.
12369            let f = match args.first() {
12370                None | Some(Value::Undef) => None,
12371                Some(_) => {
12372                    let d = arg_num(&args, 0).trunc();
12373                    if !(0.0..=100.0).contains(&d) {
12374                        return Err(host::range_error(
12375                            "toExponential() argument must be between 0 and 100",
12376                        ));
12377                    }
12378                    Some(d as usize)
12379                }
12380            };
12381            Ok(new_s(to_exponential(n, f)))
12382        }
12383        "toString" => {
12384            // An out-of-range radix THROWS; it does not silently fall back to
12385            // base 10. `(1).toString(37)` returned "1" here, so a support probe
12386            // was told every radix worked.
12387            let radix = match args.first() {
12388                None | Some(Value::Undef) => 10,
12389                Some(_) => {
12390                    let r = arg_num(&args, 0);
12391                    let t = r.trunc();
12392                    if !(2.0..=36.0).contains(&t) {
12393                        return Err(host::range_error(RADIX_RANGE));
12394                    }
12395                    t as u32
12396                }
12397            };
12398            if radix == 10 {
12399                Ok(new_s(host::fmt_number(n)))
12400            } else {
12401                Ok(new_s(to_radix(n, radix)))
12402            }
12403        }
12404        "toPrecision" => {
12405            // `undefined` (or a missing argument) behaves like `toString()`.
12406            match args.first() {
12407                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12408                Some(_) => {
12409                    let p = arg_num(&args, 0).trunc();
12410                    if !(1.0..=100.0).contains(&p) {
12411                        return Err(host::range_error(
12412                            "toPrecision() argument must be between 1 and 100",
12413                        ));
12414                    }
12415                    Ok(new_s(to_precision(n, p as usize)))
12416                }
12417            }
12418        }
12419        "toLocaleString" => Ok(new_s(to_locale_string(n))),
12420        "valueOf" => Ok(Value::Float(n)),
12421        _ => Err(host::type_error(&format!("{name} is not a function"))),
12422    }
12423}
12424
12425/// `Number.prototype.toLocaleString()` with the default locale and options:
12426/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12427/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12428/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12429/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12430/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12431fn to_locale_string(n: f64) -> String {
12432    if n.is_nan() {
12433        return "NaN".to_string();
12434    }
12435    if n.is_infinite() {
12436        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12437    }
12438    let neg = n.is_sign_negative();
12439    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12440    // (and a bare trailing point). `to_fixed` rounds half away from zero.
12441    // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12442    // which is exponential — and the grouping below then chopped up the
12443    // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12444    // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12445    // source: node groups the shortest decimal form, so `(1e100)
12446    // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12447    // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12448    // deliberately different rule — see `bigint_ctor`.)
12449    let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12450    let trimmed = match fixed.split_once('.') {
12451        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12452        None => fixed.as_str(),
12453    };
12454    let (int_part, frac_part) = match trimmed.split_once('.') {
12455        Some((i, f)) => (i, Some(f)),
12456        None => (trimmed, None),
12457    };
12458    let mut out = String::new();
12459    if neg {
12460        out.push('-'); // Intl keeps the sign even for -0.
12461    }
12462    out.push_str(&group_thousands(int_part));
12463    if let Some(f) = frac_part {
12464        out.push('.');
12465        out.push_str(f);
12466    }
12467    out
12468}
12469
12470/// Write a nonnegative decimal string in plain positional form, expanding an
12471/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12472/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12473/// negative exponent (a magnitude below 1, which the caller has already rounded
12474/// to zero), is returned unchanged.
12475fn expand_exponential(s: &str) -> String {
12476    let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12477        return s.to_string();
12478    };
12479    let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12480        return s.to_string();
12481    };
12482    if exp <= 0 {
12483        return s.to_string();
12484    }
12485    let (int_digits, frac_digits) = match mantissa.split_once('.') {
12486        Some((i, f)) => (i.to_string(), f.to_string()),
12487        None => (mantissa.to_string(), String::new()),
12488    };
12489    let mut digits = int_digits;
12490    digits.push_str(&frac_digits);
12491    // The exponent consumes the fractional digits first; whatever is left
12492    // becomes trailing zeros.
12493    let zeros = exp as usize - frac_digits.len().min(exp as usize);
12494    digits.push_str(&"0".repeat(zeros));
12495    digits
12496}
12497
12498/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12499fn group_thousands(int_part: &str) -> String {
12500    let bytes = int_part.as_bytes();
12501    let n = bytes.len();
12502    let mut out = String::with_capacity(n + n / 3);
12503    for (i, &b) in bytes.iter().enumerate() {
12504        if i > 0 && (n - i) % 3 == 0 {
12505            out.push(',');
12506        }
12507        out.push(b as char);
12508    }
12509    out
12510}
12511
12512/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12513/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12514/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12515/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12516///
12517/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12518/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12519/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12520fn to_fixed(n: f64, f: usize) -> String {
12521    if !n.is_finite() {
12522        return host::fmt_number(n);
12523    }
12524    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12525    if n.abs() >= 1e21 {
12526        return host::fmt_number(n);
12527    }
12528    let neg = n < 0.0;
12529    // Exact decimal with guard digits past the rounding position; then round the
12530    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12531    let full = format!("{:.*}", f + 25, n.abs());
12532    let mut body = round_decimal_string(&full, f);
12533    if neg {
12534        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12535    }
12536    body
12537}
12538
12539/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12540/// fractional digits, half away from zero, propagating carry across the point.
12541fn round_decimal_string(s: &str, f: usize) -> String {
12542    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12543    let mut digits: Vec<u8> = int_part
12544        .bytes()
12545        .chain(frac_part.bytes())
12546        .map(|b| b - b'0')
12547        .collect();
12548    let point = int_part.len(); // digits before the decimal point
12549    let keep = point + f; // number of leading digits to keep
12550
12551    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12552    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12553        let mut i = keep;
12554        loop {
12555            if i == 0 {
12556                digits.insert(0, 1);
12557                // A new leading digit shifts the decimal point right by one.
12558                return assemble_decimal(&digits, point + 1, f);
12559            }
12560            i -= 1;
12561            if digits[i] == 9 {
12562                digits[i] = 0;
12563            } else {
12564                digits[i] += 1;
12565                break;
12566            }
12567        }
12568    }
12569    assemble_decimal(&digits, point, f)
12570}
12571
12572/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12573/// `point` digits precede the decimal point.
12574fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12575    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12576    let int_str = int_str.trim_start_matches('0');
12577    let int_str = if int_str.is_empty() { "0" } else { int_str };
12578    if f == 0 {
12579        return int_str.to_string();
12580    }
12581    let frac: String = digits[point..point + f]
12582        .iter()
12583        .map(|d| (d + b'0') as char)
12584        .collect();
12585    format!("{int_str}.{frac}")
12586}
12587
12588/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12589/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12590/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12591/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12592/// exact digits are taken with guard positions and rounded here.
12593fn round_significant(a: f64, p: usize) -> (String, i32) {
12594    let sci = format!("{a:.*e}", p - 1 + 25);
12595    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12596    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12597    let all: Vec<u8> = mant
12598        .chars()
12599        .filter(|c| c.is_ascii_digit())
12600        .map(|c| c as u8 - b'0')
12601        .collect();
12602    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12603    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12604        // Round the p-digit mantissa up, propagating carry; a carry out of the
12605        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12606        let mut d: Vec<u8> = all[..p].to_vec();
12607        let mut i = p;
12608        loop {
12609            if i == 0 {
12610                d.insert(0, 1);
12611                d.truncate(p);
12612                e += 1;
12613                break;
12614            }
12615            i -= 1;
12616            if d[i] == 9 {
12617                d[i] = 0;
12618            } else {
12619                d[i] += 1;
12620                break;
12621            }
12622        }
12623        s = d.iter().map(|x| (x + b'0') as char).collect();
12624    }
12625    (s, e)
12626}
12627
12628/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12629/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12630/// `f` omitted, as many digits as uniquely identify the value are used
12631/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12632/// on the exact value, matching `toPrecision`.
12633fn to_exponential(n: f64, f: Option<usize>) -> String {
12634    if !n.is_finite() {
12635        return host::fmt_number(n);
12636    }
12637    let neg = n < 0.0;
12638    let a = n.abs();
12639    let (s, e) = if a == 0.0 {
12640        // Zero has no significant digits: emit "0" padded to the requested width.
12641        ("0".repeat(f.unwrap_or(0) + 1), 0)
12642    } else {
12643        match f {
12644            Some(f) => round_significant(a, f + 1),
12645            None => {
12646                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12647                let sci = format!("{a:e}");
12648                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12649                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12650                let trimmed = digits.trim_end_matches('0');
12651                let digits = if trimmed.is_empty() { "0" } else { trimmed };
12652                (digits.to_string(), exp_str.parse().unwrap_or(0))
12653            }
12654        }
12655    };
12656    let sign = if e >= 0 { '+' } else { '-' };
12657    let mag = e.abs();
12658    let body = if s.len() == 1 {
12659        format!("{s}e{sign}{mag}")
12660    } else {
12661        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12662    };
12663    if neg {
12664        format!("-{body}")
12665    } else {
12666        body
12667    }
12668}
12669
12670/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12671/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12672/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12673/// retained (`(100).toPrecision(5) === "100.00"`).
12674fn to_precision(n: f64, p: usize) -> String {
12675    if !n.is_finite() {
12676        return host::fmt_number(n);
12677    }
12678    if n == 0.0 {
12679        return if p == 1 {
12680            "0".into()
12681        } else {
12682            format!("0.{}", "0".repeat(p - 1))
12683        };
12684    }
12685    let neg = n < 0.0;
12686    let (s, e) = round_significant(n.abs(), p);
12687    let pp = p as i32;
12688
12689    let body = if e < -6 || e >= pp {
12690        // Exponential: first digit, optional '.rest', signed exponent.
12691        let sign = if e >= 0 { '+' } else { '-' };
12692        let mag = e.abs();
12693        if p == 1 {
12694            format!("{s}e{sign}{mag}")
12695        } else {
12696            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12697        }
12698    } else if e >= 0 {
12699        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12700        let ip = (e + 1) as usize;
12701        if ip == p {
12702            s
12703        } else {
12704            format!("{}.{}", &s[..ip], &s[ip..])
12705        }
12706    } else {
12707        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12708        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12709    };
12710    if neg {
12711        format!("-{body}")
12712    } else {
12713        body
12714    }
12715}
12716
12717/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12718/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12719/// is emitted exact, and fractional digits are produced up to the input double's
12720/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12721/// carry-over back into already-written digits (and into the integer part).
12722fn to_radix(n: f64, radix: u32) -> String {
12723    if !n.is_finite() {
12724        return host::fmt_number(n);
12725    }
12726    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12727    let rf = radix as f64;
12728    let neg = n < 0.0;
12729    let value = n.abs();
12730
12731    let mut integer = value.floor();
12732    let mut fraction = value - integer;
12733
12734    // Fraction digits, most-significant first.
12735    let mut frac: Vec<u8> = Vec::new();
12736    // Only compute fractional digits down to the input double's precision.
12737    let mut delta = 0.5 * (next_up(value) - value);
12738    delta = delta.max(next_up(0.0));
12739    if fraction >= delta {
12740        loop {
12741            // Shift up by one digit.
12742            fraction *= rf;
12743            delta *= rf;
12744            let digit = fraction as usize;
12745            frac.push(digits[digit]);
12746            fraction -= digit as f64;
12747            // Round to even.
12748            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12749                // Carry-over: back-trace already-written fraction digits.
12750                loop {
12751                    match frac.pop() {
12752                        None => {
12753                            // Carried past the point into the integer part.
12754                            integer += 1.0;
12755                            break;
12756                        }
12757                        Some(c) => {
12758                            let d = if c > b'9' {
12759                                (c - b'a' + 10) as u32
12760                            } else {
12761                                (c - b'0') as u32
12762                            };
12763                            if d + 1 < radix {
12764                                frac.push(digits[(d + 1) as usize]);
12765                                break;
12766                            }
12767                            // digit was radix-1: drop it and keep carrying.
12768                        }
12769                    }
12770                }
12771                break;
12772            }
12773            if fraction < delta {
12774                break;
12775            }
12776        }
12777    }
12778
12779    // Integer digits, least-significant first (reversed at the end).
12780    let mut int_out: Vec<u8> = Vec::new();
12781    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12782    while v8_exponent(integer / rf) > 0 {
12783        integer /= rf;
12784        int_out.push(b'0');
12785    }
12786    loop {
12787        let remainder = integer % rf;
12788        int_out.push(digits[remainder as usize]);
12789        integer = (integer - remainder) / rf;
12790        if integer <= 0.0 {
12791            break;
12792        }
12793    }
12794    int_out.reverse();
12795
12796    let mut out: Vec<u8> = Vec::new();
12797    if neg {
12798        out.push(b'-');
12799    }
12800    out.extend_from_slice(&int_out);
12801    if !frac.is_empty() {
12802        out.push(b'.');
12803        out.extend_from_slice(&frac);
12804    }
12805    String::from_utf8(out).unwrap()
12806}
12807
12808/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12809fn next_up(x: f64) -> f64 {
12810    f64::from_bits(x.to_bits() + 1)
12811}
12812
12813/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12814/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12815fn v8_exponent(x: f64) -> i32 {
12816    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12817    if biased == 0 {
12818        -1074 // denormal
12819    } else {
12820        biased - 1075
12821    }
12822}
12823
12824// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12825
12826/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12827/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12828/// but the value kept alongside it is what iteration and `console.log` report,
12829/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12830fn normalize_zero_key(v: Value) -> Value {
12831    match v {
12832        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12833        other => other,
12834    }
12835}
12836
12837fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12838    match name {
12839        "get" => {
12840            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12841            Ok(with_host(|h| match h.get(recv) {
12842                Some(JsObj::Map { entries, .. }) => entries
12843                    .get(&key)
12844                    .map(|(_, v)| v.clone())
12845                    .unwrap_or(Value::Undef),
12846                _ => Value::Undef,
12847            }))
12848        }
12849        "set" => {
12850            let kv = normalize_zero_key(arg0(&args));
12851            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12852            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12853            let key = with_host(|h| host::map_key(h, &kv));
12854            with_host(|h| {
12855                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12856                    entries.insert(key, (kv, vv));
12857                }
12858            });
12859            Ok(recv.clone())
12860        }
12861        "has" => {
12862            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12863            Ok(Value::Bool(with_host(
12864                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12865            )))
12866        }
12867        "delete" => {
12868            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12869            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12870                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12871                _ => false,
12872            })))
12873        }
12874        "clear" => {
12875            with_host(|h| {
12876                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12877                    entries.clear();
12878                }
12879            });
12880            Ok(Value::Undef)
12881        }
12882        "forEach" => {
12883            let cb = arg0(&args);
12884            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12885                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12886                _ => Vec::new(),
12887            });
12888            for (k, v) in pairs {
12889                host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12890            }
12891            Ok(Value::Undef)
12892        }
12893        // LIVE, not a snapshot: an entry added during iteration is visited and
12894        // one deleted before it is reached is not.
12895        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12896            recv,
12897            if name == "@@iterator" {
12898                "entries"
12899            } else {
12900                name
12901            },
12902        )),
12903        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12904    }
12905}
12906
12907/// A weak collection can only hold objects (and unregistered symbols) — a
12908/// primitive key is a `TypeError`, which is how packages probe for weak support.
12909fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12910    let weak = with_host(|h| {
12911        matches!(
12912            h.get(recv),
12913            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12914        )
12915    });
12916    if !weak {
12917        return Ok(());
12918    }
12919    let is_object = with_host(|h| match key {
12920        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12921        _ => false,
12922    });
12923    if is_object {
12924        return Ok(());
12925    }
12926    Err(host::type_error(if kind == "WeakMap" {
12927        "Invalid value used as weak map key"
12928    } else {
12929        "Invalid value used in weak set"
12930    }))
12931}
12932
12933/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12934///
12935/// The seven set operations do NOT require a real `Set` on the right-hand side:
12936/// anything with a numeric `size` and callable `has`/`keys` participates, which
12937/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12938/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12939/// diagnostic, so a bad operand reports which field was wrong rather than
12940/// failing later inside the iteration.
12941struct SetRecord {
12942    obj: Value,
12943    /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12944    /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12945    /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12946    size: f64,
12947    has: Value,
12948    keys: Value,
12949}
12950
12951fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12952    if !with_host(|h| is_object_like(h, other)) {
12953        return Err(host::type_error(&format!(
12954            "Set.prototype.{method} argument must be an object"
12955        )));
12956    }
12957    let raw = get_property(other, "size")?;
12958    let num = host::to_number_value(&raw)?;
12959    if num.is_nan() {
12960        return Err(host::type_error("The .size property is NaN"));
12961    }
12962    let size = num.trunc();
12963    if size < 0.0 {
12964        return Err(host::range_error(&format!("'{size}' is an invalid size")));
12965    }
12966    let has = get_property(other, "has")?;
12967    if !with_host(|h| host::is_callable(h, &has)) {
12968        return Err(host::type_error("string \"has\" is not a function"));
12969    }
12970    let keys = get_property(other, "keys")?;
12971    if !with_host(|h| host::is_callable(h, &keys)) {
12972        return Err(host::type_error("string \"keys\" is not a function"));
12973    }
12974    Ok(SetRecord {
12975        obj: other.clone(),
12976        size,
12977        has,
12978        keys,
12979    })
12980}
12981
12982impl SetRecord {
12983    /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12984    /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12985    fn has(&self, v: &Value) -> Result<bool, String> {
12986        let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12987        Ok(with_host(|h| h.truthy(&r)))
12988    }
12989
12990    /// The operand's elements, drained from the iterator its `keys` method
12991    /// returns. A non-object result is the spec's `Result of the keys method is
12992    /// not an object`, reported before anything is iterated.
12993    fn keys(&self) -> Result<Vec<Value>, String> {
12994        let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
12995        if !with_host(|h| is_object_like(h, &it)) {
12996            return Err(host::type_error(
12997                "Result of the keys method is not an object",
12998            ));
12999        }
13000        host::drain_iterator(&it)
13001    }
13002}
13003
13004/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13005/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13006/// LEFT even though it can on the right.
13007fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13008    if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13009        return Ok(());
13010    }
13011    Err(host::type_error(&format!(
13012        "Method Set.prototype.{method} called on incompatible receiver {}",
13013        with_host(|h| object_tag(h, recv))
13014    )))
13015}
13016
13017/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13018///
13019/// Every one of these operations copies `[[SetData]]` *after* it has touched
13020/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13021/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13022/// result. Snapshotting the receiver up front instead dropped such an element:
13023/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13024fn set_values(recv: &Value) -> Vec<Value> {
13025    with_host(|h| match h.get(recv) {
13026        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13027        _ => Vec::new(),
13028    })
13029}
13030
13031fn set_size(recv: &Value) -> f64 {
13032    with_host(|h| match h.get(recv) {
13033        Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13034        _ => 0.0,
13035    })
13036}
13037
13038/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13039/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13040fn new_set(items: Vec<Value>) -> Result<Value, String> {
13041    let s = with_host(|h| {
13042        h.alloc(JsObj::Set {
13043            entries: IndexMap::new(),
13044            weak: false,
13045        })
13046    });
13047    for v in items {
13048        set_method(&s, "add", vec![v])?;
13049    }
13050    Ok(s)
13051}
13052
13053fn set_contains(s: &Value, v: &Value) -> bool {
13054    let key = with_host(|h| host::map_key(h, v));
13055    with_host(
13056        |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13057    )
13058}
13059
13060/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13061///
13062/// Each one branches on the two sizes and iterates the SMALLER side — not an
13063/// optimization but observable behaviour: which side is walked decides the
13064/// result's order and whether the operand's `has` or its `keys` is the method
13065/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13066/// yields the operand's order, and its `keys` (never its `has`) is called.
13067fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13068    require_set_receiver(recv, name)?;
13069    let other = get_set_record(&arg0(&args), name)?;
13070    let my_size = set_size(recv);
13071    match name {
13072        "union" => {
13073            let keys = other.keys()?;
13074            let mut out = set_values(recv);
13075            out.extend(keys);
13076            new_set(out)
13077        }
13078        "intersection" => {
13079            let mut out = Vec::new();
13080            if my_size <= other.size {
13081                for v in set_values(recv) {
13082                    if other.has(&v)? {
13083                        out.push(v);
13084                    }
13085                }
13086            } else {
13087                for k in other.keys()? {
13088                    if set_contains(recv, &k) {
13089                        out.push(k);
13090                    }
13091                }
13092            }
13093            new_set(out)
13094        }
13095        "difference" => {
13096            if my_size <= other.size {
13097                let mut out = Vec::new();
13098                for v in set_values(recv) {
13099                    if !other.has(&v)? {
13100                        out.push(v);
13101                    }
13102                }
13103                return new_set(out);
13104            }
13105            let out = new_set(set_values(recv))?;
13106            for k in other.keys()? {
13107                set_method(&out, "delete", vec![k])?;
13108            }
13109            Ok(out)
13110        }
13111        "symmetricDifference" => {
13112            // The operand is drained FIRST — the spec takes the iterator before
13113            // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13114            // reflected in the result.
13115            let keys = other.keys()?;
13116            let out = new_set(set_values(recv))?;
13117            for k in keys {
13118                if set_contains(recv, &k) {
13119                    set_method(&out, "delete", vec![k])?;
13120                } else {
13121                    set_method(&out, "add", vec![k])?;
13122                }
13123            }
13124            Ok(out)
13125        }
13126        "isSubsetOf" => {
13127            if my_size > other.size {
13128                return Ok(Value::Bool(false));
13129            }
13130            for v in set_values(recv) {
13131                if !other.has(&v)? {
13132                    return Ok(Value::Bool(false));
13133                }
13134            }
13135            Ok(Value::Bool(true))
13136        }
13137        "isSupersetOf" => {
13138            if my_size < other.size {
13139                return Ok(Value::Bool(false));
13140            }
13141            for k in other.keys()? {
13142                if !set_contains(recv, &k) {
13143                    return Ok(Value::Bool(false));
13144                }
13145            }
13146            Ok(Value::Bool(true))
13147        }
13148        "isDisjointFrom" => {
13149            if my_size <= other.size {
13150                for v in set_values(recv) {
13151                    if other.has(&v)? {
13152                        return Ok(Value::Bool(false));
13153                    }
13154                }
13155            } else {
13156                for k in other.keys()? {
13157                    if set_contains(recv, &k) {
13158                        return Ok(Value::Bool(false));
13159                    }
13160                }
13161            }
13162            Ok(Value::Bool(true))
13163        }
13164        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13165    }
13166}
13167
13168fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13169    match name {
13170        "add" => {
13171            let vv = normalize_zero_key(arg0(&args));
13172            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13173            let key = with_host(|h| host::map_key(h, &vv));
13174            with_host(|h| {
13175                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13176                    entries.insert(key, vv);
13177                }
13178            });
13179            Ok(recv.clone())
13180        }
13181        "has" => {
13182            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13183            Ok(Value::Bool(with_host(
13184                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13185            )))
13186        }
13187        "delete" => {
13188            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13189            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13190                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13191                _ => false,
13192            })))
13193        }
13194        "clear" => {
13195            with_host(|h| {
13196                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13197                    entries.clear();
13198                }
13199            });
13200            Ok(Value::Undef)
13201        }
13202        "forEach" => {
13203            let cb = arg0(&args);
13204            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13205                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13206                _ => Vec::new(),
13207            });
13208            for v in vals {
13209                host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13210            }
13211            Ok(Value::Undef)
13212        }
13213        "union"
13214        | "intersection"
13215        | "difference"
13216        | "symmetricDifference"
13217        | "isSubsetOf"
13218        | "isSupersetOf"
13219        | "isDisjointFrom" => set_operation(recv, name, args),
13220        // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13221        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13222            recv,
13223            if name == "entries" {
13224                "entries"
13225            } else {
13226                "values"
13227            },
13228        )),
13229        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13230    }
13231}
13232
13233fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13234    // A generator IS its own iterator: both symbol forms return the receiver.
13235    if matches!(name, "@@iterator" | "@@asyncIterator") {
13236        return Ok(recv.clone());
13237    }
13238    // An `async function*` object's methods return PROMISES of the record, and
13239    // its body has to be driven through the await-aware stepper (a plain
13240    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13241    if host::is_async_generator(recv) {
13242        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13243        // `.return`/`.throw` must wait behind a `.next()` that is still
13244        // suspended on an internal `await`, or that `.next()` would report
13245        // `{done: true}` for a value the body had not yet reached. An uncaught
13246        // `.throw(e)` rejects the returned promise; it does not throw here.
13247        return match name {
13248            "next" => Ok(host::async_gen_enqueue(
13249                recv,
13250                host::GenReq::Next(arg0(&args)),
13251            )),
13252            "return" => Ok(host::async_gen_enqueue(
13253                recv,
13254                host::GenReq::Return(arg0(&args)),
13255            )),
13256            "throw" => Ok(host::async_gen_enqueue(
13257                recv,
13258                host::GenReq::Throw(arg0(&args)),
13259            )),
13260            "@@asyncIterator" => Ok(recv.clone()),
13261            _ => Err(host::type_error(&format!(
13262                "asyncGenerator.{name} is not a function"
13263            ))),
13264        };
13265    }
13266    match name {
13267        "next" => {
13268            let send = arg0(&args);
13269            match host::gen_resume(recv, send)? {
13270                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13271                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13272            }
13273        }
13274        "return" => {
13275            // Resume with an injected return so any pending `finally` runs; the
13276            // completion may itself be a `finally` yield (not-done) or the value.
13277            match host::gen_return(recv, arg0(&args))? {
13278                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13279                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13280            }
13281        }
13282        "throw" => {
13283            // Inject a throw at the suspension point: an enclosing `try/catch` in
13284            // the body can handle it (and any `finally` runs); otherwise it
13285            // propagates to the caller.
13286            match host::gen_throw(recv, arg0(&args))? {
13287                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13288                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13289            }
13290        }
13291        _ => Err(host::type_error(&format!(
13292            "generator.{name} is not a function"
13293        ))),
13294    }
13295}
13296
13297/// A `{ value, done }` iterator-result object.
13298fn iter_result(value: Value, done: bool) -> Value {
13299    with_host(|h| {
13300        let mut m: IndexMap<String, Value> = IndexMap::new();
13301        m.insert("value".into(), value);
13302        m.insert("done".into(), Value::Bool(done));
13303        h.new_object(m)
13304    })
13305}
13306
13307/// A live iterator over array `arr` — what `keys()`, `values()`, `entries()`
13308/// and `Symbol.iterator` return, and what a `for-of` over an array steps.
13309pub(crate) fn array_iterator(arr: &Value, kind: host::ArrayIterKind) -> Value {
13310    with_host(|h| {
13311        h.alloc(JsObj::Iter {
13312            items: Vec::new(),
13313            idx: 0,
13314            array: Some((arr.clone(), kind)),
13315        })
13316    })
13317}
13318
13319/// One step of a `JsObj::Iter`: `None` when `it` is not one, `Some(None)` once
13320/// it is exhausted, otherwise the next value.
13321///
13322/// An array iterator reads the array at every step (23.1.5.1
13323/// `%ArrayIteratorPrototype%.next`): the length is re-read, so an element
13324/// pushed during a `for-of` is visited and one popped is not, and the element
13325/// is read as `a[i]` reads it — an accessor runs, a hole reads through the
13326/// prototype. Once it reports done it stays done, even if the array grows.
13327pub(crate) fn iter_step(it: &Value) -> Option<Option<Value>> {
13328    use host::ArrayIterKind;
13329    // One host borrow for the common case — a snapshot, or an array slot that
13330    // is neither a hole nor an accessor. `Err` carries what only `[[Get]]` can
13331    // read: the array, the kind and the index.
13332    let step = with_host(|h| {
13333        let (arr, kind, i) = match h.get_mut(it) {
13334            Some(JsObj::Iter {
13335                items,
13336                idx,
13337                array: None,
13338            }) => {
13339                let v = items.get(*idx).cloned();
13340                if v.is_some() {
13341                    *idx += 1;
13342                }
13343                return Some(Ok(v));
13344            }
13345            Some(JsObj::Iter {
13346                idx,
13347                array: Some((arr, kind)),
13348                ..
13349            }) => (arr.clone(), *kind, *idx),
13350            _ => return None,
13351        };
13352        // `usize::MAX` marks an iterator that has already reported done, and
13353        // it stays done even if the array grows.
13354        let len = match h.get(&arr) {
13355            Some(JsObj::Array(items)) => items.len(),
13356            _ => 0,
13357        };
13358        let done = i == usize::MAX || i >= len;
13359        if let Some(JsObj::Iter { idx, .. }) = h.get_mut(it) {
13360            *idx = if done { usize::MAX } else { i + 1 };
13361        }
13362        if done {
13363            return Some(Ok(None));
13364        }
13365        let key = Value::Float(i as f64);
13366        let slot = match (kind, h.get(&arr)) {
13367            (ArrayIterKind::Keys, _) => return Some(Ok(Some(key))),
13368            (_, Some(JsObj::Array(items)))
13369                if !h.is_hole(&arr, i) && h.own_accessor_keys(&arr).is_empty() =>
13370            {
13371                items[i].clone()
13372            }
13373            _ => return Some(Err((arr, kind, i))),
13374        };
13375        Some(Ok(Some(match kind {
13376            ArrayIterKind::Entries => h.new_array(vec![key, slot]),
13377            _ => slot,
13378        })))
13379    })?;
13380    let (arr, kind, i) = match step {
13381        Ok(step) => return Some(step),
13382        Err(slow) => slow,
13383    };
13384    let value = get_property(&arr, &i.to_string()).unwrap_or(Value::Undef);
13385    Some(Some(match kind {
13386        ArrayIterKind::Entries => with_host(|h| h.new_array(vec![Value::Float(i as f64), value])),
13387        _ => value,
13388    }))
13389}
13390
13391/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a
13392/// cursor over a snapshot, or live over an array ([`iter_step`]).
13393fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13394    match name {
13395        "next" => Ok(match iter_step(recv).flatten() {
13396            Some(v) => iter_result(v, false),
13397            None => iter_result(Value::Undef, true),
13398        }),
13399        "return" => {
13400            // Exhaust the cursor and report done.
13401            with_host(|h| {
13402                if let Some(JsObj::Iter { items, idx, array }) = h.get_mut(recv) {
13403                    *idx = if array.is_some() {
13404                        usize::MAX
13405                    } else {
13406                        items.len()
13407                    };
13408                }
13409            });
13410            Ok(iter_result(arg0(&args), true))
13411        }
13412        // An iterator is its own iterable.
13413        "@@iterator" => Ok(recv.clone()),
13414        _ => Err(host::type_error(&format!(
13415            "iterator.{name} is not a function"
13416        ))),
13417    }
13418}
13419
13420fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13421    match name {
13422        "toString" => Ok(with_host(|h| {
13423            let s = h.str_of(recv);
13424            h.new_str(s)
13425        })),
13426        // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13427        // itself for EVERY hint — it ignores its argument. That is what makes
13428        // `sym + ''` a TypeError rather than a concatenation: the conversion
13429        // succeeds and hands back a symbol, and it is `+` that then rejects it.
13430        "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13431        _ => Err(host::type_error(&format!(
13432            "symbol.{name} is not a function"
13433        ))),
13434    }
13435}
13436
13437// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13438
13439fn object_create(args: Vec<Value>) -> Result<Value, String> {
13440    let proto = arg0(&args);
13441    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13442    // `undefined` is NOT accepted — measured on node v26.7.0,
13443    // `Object.create(undefined)` is
13444    // `TypeError: Object prototype may only be an Object or null: undefined`,
13445    // where node-js quietly built a normal object.
13446    reject_bad_prototype(&proto)?;
13447    let obj = with_host(|h| h.new_object(IndexMap::new()));
13448    // `set_proto` records a null proto as an explicit null-prototype object.
13449    with_host(|h| h.set_proto(&obj, proto));
13450    // Optional second arg: a property-descriptor map.
13451    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13452        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13453            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13454            _ => Vec::new(),
13455        });
13456        for (k, d) in entries {
13457            apply_descriptor(&obj, &k, &d)?;
13458        }
13459    }
13460    Ok(obj)
13461}
13462
13463/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13464/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13465/// `EventEmitter.prototype` (the one express mixes onto its app function).
13466/// The own property names of `<Ctor>.prototype`, and whether each is
13467/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13468///
13469/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13470/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13471/// but were not enumerable, so feature detection that walks a prototype found
13472/// nothing there. The table is read from the reference engine rather than
13473/// derived from the arity table because the arity table holds functions only:
13474/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13475/// `URL.prototype` components are accessors.
13476fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13477    let ctor = ns.strip_suffix(".prototype")?;
13478    crate::arity::PROTO_MEMBERS
13479        .binary_search_by(|(k, _)| (*k).cmp(ctor))
13480        .ok()
13481        .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13482}
13483
13484fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13485    match ns {
13486        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13487        _ => None,
13488    }
13489}
13490
13491/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13492/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13493/// filtered out of); every other receiver answers from its property map.
13494fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13495    if let Some(keys) = crate::proxy::own_keys(v)? {
13496        return Ok(keys
13497            .iter()
13498            .filter(|k| host::is_symbol_key(k))
13499            .map(|k| crate::proxy::key_value(k))
13500            .collect());
13501    }
13502    // An intrinsic prototype's symbol-keyed members come from the generated
13503    // table, which is the only record of them: they own no map entry, so
13504    // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13505    // reports `Symbol.iterator` and `Symbol.unscopables`.
13506    if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13507        if let Some(members) = intrinsic_proto_members(&ns) {
13508            return Ok(with_host(|h| {
13509                members
13510                    .iter()
13511                    .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13512                    .map(|name| h.well_known_symbol(name))
13513                    .collect()
13514            }));
13515        }
13516    }
13517    Ok(with_host(|h| h.own_symbol_keys(v)))
13518}
13519
13520/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13521pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13522    object_define_property(vec![obj.clone(), key, desc])
13523}
13524
13525/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13526pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13527    object_get_own_descriptor(vec![obj.clone(), key])
13528}
13529
13530fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13531    let obj = arg0(&args);
13532    // A Proxy defines through its `defineProperty` trap; the target it forwards
13533    // to is where the ordinary path below finally runs.
13534    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13535        let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13536        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13537        if !with_host(|h| is_object_like(h, &desc)) {
13538            return Err(host::type_error(&format!(
13539                "Property description must be an object: {}",
13540                with_host(|h| h.str_of(&desc))
13541            )));
13542        }
13543        // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13544        // too. `Reflect.defineProperty` is the form that reports `false`.
13545        if !crate::proxy::define_property(&obj, &key, &desc)? {
13546            return Err(host::type_error(&format!(
13547                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13548            )));
13549        }
13550        return Ok(obj);
13551    }
13552    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13553    // target and a non-object descriptor each throw before anything is written.
13554    if !with_host(|h| is_object_like(h, &obj)) {
13555        return Err(host::type_error(
13556            "Object.defineProperty called on non-object",
13557        ));
13558    }
13559    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13560    if !with_host(|h| is_object_like(h, &desc)) {
13561        return Err(host::type_error(&format!(
13562            "Property description must be an object: {}",
13563            with_host(|h| h.str_of(&desc))
13564        )));
13565    }
13566    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13567    apply_descriptor(&obj, &key, &desc)?;
13568    Ok(obj)
13569}
13570
13571/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13572/// for anything else (28.1). A primitive was being accepted and silently
13573/// producing nothing.
13574/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13575/// `Reflect.construct` take.
13576///
13577/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13578/// iterator was being used instead, so an array-like produced nothing and a
13579/// primitive produced nothing rather than the TypeError node raises.
13580/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13581/// object still accepts, because it changes nothing.
13582///
13583/// The observable prototype, not the stored link: an ordinary object has no
13584/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13585/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13586/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13587/// step 8 walks up from `p` looking for `obj`.
13588///
13589/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13590/// built a ring. Nothing hung, because every chain walk in this host carries a
13591/// hop limit, but a lookup then silently gave up instead of finding a property
13592/// that really was there.
13593fn would_cycle(obj: &Value, p: &Value) -> bool {
13594    let mut cur = Some(p.clone());
13595    for _ in 0..1000 {
13596        let Some(c) = cur else { return false };
13597        if with_host(|h| h.strict_eq(&c, obj)) {
13598            return true;
13599        }
13600        // A PROXY's prototype is its handler's business; the spec skips the
13601        // walk entirely when one is in the chain.
13602        if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13603            return false;
13604        }
13605        cur = with_host(|h| h.proto_of(&c));
13606    }
13607    false
13608}
13609
13610fn same_prototype(obj: &Value, p: &Value) -> bool {
13611    let cur = prototype_of(obj);
13612    with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13613}
13614
13615fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13616    if !with_host(|h| is_object_like(h, v)) {
13617        return Err(host::type_error(
13618            "CreateListFromArrayLike called on non-object",
13619        ));
13620    }
13621    let len = get_property(v, "length")?;
13622    let n = with_host(|h| h.to_number(&len));
13623    let n = if n.is_finite() && n > 0.0 {
13624        n as usize
13625    } else {
13626        0
13627    };
13628    (0..n).map(|i| get_property(v, &i.to_string())).collect()
13629}
13630
13631fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13632    if with_host(|h| is_object_like(h, v)) {
13633        return Ok(());
13634    }
13635    Err(host::type_error(&format!(
13636        "Reflect.{method} called on non-object"
13637    )))
13638}
13639
13640/// Whether `v` is an Object in the language sense — anything `typeof` calls
13641/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13642/// distinguish "an object" from a primitive.
13643fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13644    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13645}
13646
13647/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13648/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13649/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13650/// `assign` family: each returned an empty result for `null` where node v26.7.0
13651/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13652/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13653fn require_object_coercible(v: &Value) -> Result<(), String> {
13654    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13655        return Err(host::type_error(
13656            "Cannot convert undefined or null to object",
13657        ));
13658    }
13659    Ok(())
13660}
13661
13662/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13663/// nor `null`, with V8's wording. Measured on node v26.7.0:
13664/// `Object.create("s")` is
13665/// `TypeError: Object prototype may only be an Object or null: s`.
13666fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13667    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13668        return Ok(());
13669    }
13670    Err(host::type_error(&format!(
13671        "Object prototype may only be an Object or null: {}",
13672        with_host(|h| h.str_of(proto))
13673    )))
13674}
13675
13676/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13677///
13678/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13679/// `configurable` field defaults to **false** — which is why a `defineProperty`
13680/// data property is invisible to `Object.keys` unless the caller opts in. That
13681/// asymmetry against plain assignment is the whole reason the attribute table
13682/// exists.
13683/// The requested fields of a property descriptor — 10.1.6.2
13684/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13685/// is the distinction the merge below turns on: an omitted field LEAVES an
13686/// existing attribute alone rather than resetting it.
13687struct Requested {
13688    value: Option<Value>,
13689    get: Option<Option<Value>>,
13690    set: Option<Option<Value>>,
13691    writable: Option<bool>,
13692    enumerable: Option<bool>,
13693    configurable: Option<bool>,
13694}
13695
13696impl Requested {
13697    /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13698    /// `HasProperty`/`Get` pairs do — a descriptor built with
13699    /// `Object.create({ value: 1 })` is legal.
13700    fn read(desc: &Value) -> Self {
13701        let has = |k: &str| {
13702            with_host(|h| {
13703                host::lookup_chain(h, desc, k).is_some()
13704                    || host::lookup_accessor(h, desc, k).is_some()
13705            })
13706        };
13707        let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13708        // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13709        // doing it inside the `with_host` closure aborts on the double borrow.
13710        let flag = |k: &str| {
13711            has(k).then(|| {
13712                let v = val(k);
13713                with_host(|h| h.truthy(&v))
13714            })
13715        };
13716        Requested {
13717            value: has("value").then(|| val("value")),
13718            get: has("get").then(|| match val("get") {
13719                Value::Undef => None,
13720                g => Some(g),
13721            }),
13722            set: has("set").then(|| match val("set") {
13723                Value::Undef => None,
13724                st => Some(st),
13725            }),
13726            writable: flag("writable"),
13727            enumerable: flag("enumerable"),
13728            configurable: flag("configurable"),
13729        }
13730    }
13731
13732    fn is_accessor(&self) -> bool {
13733        self.get.is_some() || self.set.is_some()
13734    }
13735
13736    fn is_data(&self) -> bool {
13737        self.value.is_some() || self.writable.is_some()
13738    }
13739}
13740
13741/// The own property already at `key`, if any, read back through
13742/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13743/// fn-prop side table, Buffer bytes) is covered by one code path.
13744struct Existing {
13745    accessor: bool,
13746    value: Value,
13747    get: Option<Value>,
13748    set: Option<Value>,
13749    writable: bool,
13750    enumerable: bool,
13751    configurable: bool,
13752}
13753
13754fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13755    let k = with_host(|h| h.new_str(key.to_string()));
13756    let d = own_descriptor_pub(obj, k).ok()?;
13757    if matches!(d, Value::Undef) {
13758        return None;
13759    }
13760    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13761    let truthy = |n: &str| {
13762        let v = field(n);
13763        with_host(|h| h.truthy(&v))
13764    };
13765    let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13766    Some(Existing {
13767        accessor,
13768        value: field("value"),
13769        get: match field("get") {
13770            Value::Undef => None,
13771            g => Some(g),
13772        },
13773        set: match field("set") {
13774            Value::Undef => None,
13775            st => Some(st),
13776        },
13777        writable: truthy("writable"),
13778        enumerable: truthy("enumerable"),
13779        configurable: truthy("configurable"),
13780    })
13781}
13782
13783/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13784/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13785/// one with this, not with strict equality.
13786pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13787    let num = |v: &Value| match v {
13788        Value::Int(n) => Some(*n as f64),
13789        Value::Float(f) => Some(*f),
13790        _ => None,
13791    };
13792    match (num(a), num(b)) {
13793        (Some(x), Some(y)) => {
13794            if x.is_nan() && y.is_nan() {
13795                true
13796            } else if x == 0.0 && y == 0.0 {
13797                x.is_sign_negative() == y.is_sign_negative()
13798            } else {
13799                x == y
13800            }
13801        }
13802        _ => with_host(|h| h.strict_eq(a, b)),
13803    }
13804}
13805
13806/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13807///
13808/// None of the validation existed: every `Object.defineProperty` was applied
13809/// unconditionally, so redefining a non-configurable property silently
13810/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13811/// `false` rather than "leave alone", so the ordinary
13812/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13813/// `writable` and `configurable` from a property that had both.
13814///
13815/// Converting an accessor to a data property did not take effect at all: the
13816/// value was written but the accessor stayed in its side table, and accessors
13817/// win on read, so the getter kept answering.
13818fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13819    let req = Requested::read(desc);
13820    let cur = existing_property(obj, key);
13821
13822    // An array's `length` is the exotic own property whose write resizes the
13823    // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13824    // key and left the elements untouched.
13825    if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13826        if let Some(v) = req.value.clone() {
13827            return set_property_pub(obj, "length", v);
13828        }
13829    }
13830
13831    // The other exotics whose own properties are SYNTHESIZED rather than stored
13832    // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13833    // The ordinary path below writes a shadowing map entry the read never
13834    // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13835    // unchanged.
13836    // A builtin namespace/prototype has no property map either, so a data
13837    // descriptor has to reach the same side table an assignment does.
13838    // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13839    // careful polyfill installs itself, precisely to avoid the enumerable
13840    // property a bare assignment creates — wrote a map entry nothing read.
13841    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13842        if let Some(v) = req.value.clone() {
13843            return set_property_pub(obj, key, v);
13844        }
13845    }
13846    let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13847        && key.parse::<usize>().is_ok())
13848        || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13849    if exotic_own {
13850        if let Some(v) = req.value.clone() {
13851            return set_property_pub(obj, key, v);
13852        }
13853    }
13854
13855    // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13856    // object. Only an existing property's attributes were being validated, so
13857    // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13858    if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13859        return Err(host::type_error(&format!(
13860            "Cannot define property {key}, object is not extensible"
13861        )));
13862    }
13863    if let Some(c) = &cur {
13864        if !c.configurable {
13865            let rejected = req.configurable == Some(true)
13866                || req.enumerable.is_some_and(|e| e != c.enumerable)
13867                || (req.is_accessor() && !c.accessor)
13868                || (req.is_data() && c.accessor)
13869                || (c.accessor
13870                    && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13871                        || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13872                || (!c.accessor
13873                    && !c.writable
13874                    && (req.writable == Some(true)
13875                        || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13876            if rejected {
13877                return Err(host::type_error(&format!(
13878                    "Cannot redefine property: {key}"
13879                )));
13880            }
13881        }
13882    }
13883
13884    // An omitted field keeps what the property already had; a brand-new
13885    // property defaults every one of them to false.
13886    let attrs = host::PropAttrs {
13887        writable: req
13888            .writable
13889            .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13890        enumerable: req
13891            .enumerable
13892            .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13893        configurable: req
13894            .configurable
13895            .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13896    };
13897    with_host(|h| h.set_prop_attrs(obj, key, attrs));
13898
13899    if req.is_accessor() {
13900        let get = req
13901            .get
13902            .clone()
13903            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13904        let set = req
13905            .set
13906            .clone()
13907            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13908        // An ACCESSOR at an index past the end still extends the array
13909        // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13910        // `length === 5` with holes between. Only the DATA path grew it, so
13911        // the accessor landed in the side table while `length` stayed put —
13912        // and with it out of range, `Object.keys` and `JSON.stringify` never
13913        // saw the index at all.
13914        if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13915        {
13916            with_host(|h| {
13917                let old_len = match h.get(obj) {
13918                    Some(JsObj::Array(items)) => items.len(),
13919                    _ => 0,
13920                };
13921                if i >= old_len {
13922                    if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13923                        items.resize(i + 1, Value::Undef);
13924                    }
13925                    h.mark_hole_range(obj, old_len..i + 1);
13926                }
13927            });
13928        }
13929        with_host(|h| h.set_accessor(obj, key, get, set));
13930        return Ok(());
13931    }
13932
13933    if let Some(c) = &cur {
13934        if c.accessor {
13935            if !req.is_data() {
13936                // A generic descriptor — flags only — leaves an accessor an
13937                // accessor. They were already applied above.
13938                return Ok(());
13939            }
13940            let v = req.value.clone().unwrap_or(Value::Undef);
13941            with_host(|h| h.accessor_to_data(obj, key, v));
13942            return Ok(());
13943        }
13944    }
13945
13946    let Some(v) = req.value else {
13947        // Nothing to write: a flags-only redefinition of a data property.
13948        return Ok(());
13949    };
13950    write_data_slot(obj, key, v);
13951    Ok(())
13952}
13953
13954/// Store `v` as an own data property, in whichever slot the object kind keeps
13955/// its own properties.
13956fn write_data_slot(obj: &Value, key: &str, v: Value) {
13957    // A function/class receiver stores its own props in the fn-prop side table
13958    // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13959    if matches!(
13960        with_host(|h| h.get(obj).cloned()),
13961        Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13962    ) || uses_side_table(obj)
13963    {
13964        with_host(|h| h.set_fn_prop(obj, key, v));
13965        return;
13966    }
13967    if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13968        // An array's index keys ARE its elements, and defining one past the end
13969        // grows the array with holes in between (10.4.2.1). This whole branch
13970        // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13971        // into the ordinary property map an array does not have, so it was a
13972        // silent no-op.
13973        with_host(|h| {
13974            let old = match h.get(obj) {
13975                Some(JsObj::Array(items)) => items.len(),
13976                _ => 0,
13977            };
13978            if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13979                if i >= old {
13980                    items.resize(i + 1, Value::Undef);
13981                }
13982                items[i] = v;
13983            }
13984            if i > old {
13985                h.mark_hole_range(obj, old..i);
13986            }
13987            h.clear_hole(obj, i);
13988        });
13989        return;
13990    }
13991    with_host(|h| {
13992        if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13993            p.insert(key.to_string(), v);
13994            host::canonicalize_own_keys(p);
13995        }
13996    });
13997}
13998
13999/// `Object.defineProperties(obj, descriptorMap)`.
14000fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
14001    let obj = arg0(&args);
14002    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
14003    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
14004        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
14005        _ => Vec::new(),
14006    });
14007    for (k, d) in entries {
14008        apply_descriptor(&obj, &k, &d)?;
14009    }
14010    Ok(obj)
14011}
14012
14013/// The descriptor of an own property a function, a typed array or a RegExp
14014/// SYNTHESIZES rather than keeping in a property map.
14015///
14016/// These read back through the ordinary path but owned no descriptor and did
14017/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
14018/// views of "does this property exist" disagreed — a read said yes while
14019/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
14020/// what a shim checks before patching.
14021fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
14022    let ro_configurable = host::PropAttrs {
14023        writable: false,
14024        enumerable: false,
14025        configurable: true,
14026    };
14027    // A callable's `length`/`name` are read-only but configurable; its
14028    // `prototype` is writable and NOT configurable, and a class's is neither.
14029    // An arrow, a method and a bound function own no `prototype` at all.
14030    if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
14031        return None;
14032    }
14033    if with_host(|h| host::is_callable(h, obj)) {
14034        if key == "prototype" {
14035            let p = get_property(obj, "prototype").ok()?;
14036            if matches!(p, Value::Undef) {
14037                return None;
14038            }
14039            return Some((
14040                p,
14041                host::PropAttrs {
14042                    writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
14043                    enumerable: false,
14044                    configurable: false,
14045                },
14046            ));
14047        }
14048        return Some((get_property(obj, key).ok()?, ro_configurable));
14049    }
14050    // A typed array's elements are own, enumerable, writable, configurable
14051    // properties; an index past the end owns nothing.
14052    if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
14053        let v = crate::stdlib::typedarray::elem_get(obj, key)?;
14054        return Some((
14055            v,
14056            host::PropAttrs {
14057                writable: true,
14058                enumerable: true,
14059                configurable: true,
14060            },
14061        ));
14062    }
14063    // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
14064    if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
14065        return Some((
14066            get_property(obj, "lastIndex").ok()?,
14067            host::PropAttrs {
14068                writable: true,
14069                enumerable: false,
14070                configurable: false,
14071            },
14072        ));
14073    }
14074    None
14075}
14076
14077fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14078    let obj = arg0(&args);
14079    require_object_coercible(&obj)?;
14080    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14081    // A string primitive's boxed own properties: each code-unit index is an
14082    // enumerable, non-writable, non-configurable data property, and `length` is
14083    // the same minus enumerable.
14084    if let Some(units) = string_primitive_units(&obj) {
14085        let entry = match key.parse::<usize>() {
14086            Ok(i) => units
14087                .get(i)
14088                .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14089            Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14090            Err(_) => None,
14091        };
14092        return Ok(match entry {
14093            Some((value, enumerable)) => with_host(|h| {
14094                let mut m: IndexMap<String, Value> = IndexMap::new();
14095                m.insert("value".into(), value);
14096                m.insert("writable".into(), Value::Bool(false));
14097                m.insert("enumerable".into(), Value::Bool(enumerable));
14098                m.insert("configurable".into(), Value::Bool(false));
14099                h.new_object(m)
14100            }),
14101            None => Value::Undef,
14102        });
14103    }
14104    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14105        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14106    }
14107    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14108    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14109    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14110        if let Some(names) = builtin_proto_method_names(&ns) {
14111            if names.contains(&key.as_str()) {
14112                return Ok(with_host(|h| {
14113                    let thunk = h.alloc(JsObj::Builtin(format!(
14114                        "@proto:{}:{key}",
14115                        ns.trim_end_matches(".prototype")
14116                    )));
14117                    let mut m: IndexMap<String, Value> = IndexMap::new();
14118                    m.insert("value".into(), thunk);
14119                    m.insert("writable".into(), Value::Bool(true));
14120                    m.insert("enumerable".into(), Value::Bool(true));
14121                    m.insert("configurable".into(), Value::Bool(true));
14122                    h.new_object(m)
14123                }));
14124            }
14125        }
14126    }
14127    // A global the object does not own outright is still an own property of the
14128    // global object — the same lazy binding the bare identifier resolves to.
14129    // Every one of them reported `undefined`, so a feature probe written as
14130    // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14131    // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14132    // rest are ordinary writable, non-enumerable, configurable bindings.
14133    if with_host(|h| h.is_global_object(&obj)) {
14134        let owned = with_host(|h| match h.get(&obj) {
14135            Some(JsObj::Object(p)) => p.contains_key(&key),
14136            _ => false,
14137        });
14138        if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14139            // A global a SCRIPT created — `x = 1` with no declaration — is an
14140            // ordinary enumerable property, unlike the builtins.
14141            let script_made = with_host(|h| h.read_global(&key).is_some());
14142            if let Some(v) = global_object_binding(&key) {
14143                let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14144                return Ok(with_host(|h| {
14145                    let mut m: IndexMap<String, Value> = IndexMap::new();
14146                    m.insert("value".into(), v);
14147                    m.insert("writable".into(), Value::Bool(!frozen));
14148                    m.insert(
14149                        "enumerable".into(),
14150                        Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14151                    );
14152                    m.insert("configurable".into(), Value::Bool(!frozen));
14153                    h.new_object(m)
14154                }));
14155            }
14156        }
14157    }
14158    // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14159    // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14160    // one of these reads back a value, but none owned a DESCRIPTOR:
14161    // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14162    // as "no such property" to the shim/polyfill family that probes a namespace
14163    // before patching it.
14164    // An ACCESSOR member describes itself with a `get`, never a `value` — and
14165    // it must do so without READING the property, since running the getter
14166    // against the prototype is exactly what throws. Both prototype
14167    // representations are covered, so `Symbol.prototype.description` and
14168    // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14169    // "no such property" to anything that probes before patching.
14170    if let Some(ctor) = intrinsic_proto_of(&obj) {
14171        if is_proto_accessor(&ctor, &key) {
14172            let getter = proto_getter(&ctor, &key);
14173            // The poison pair is the only ECMAScript accessor here with a
14174            // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14175            // `hostname` and the rest are all writable, and reporting them as
14176            // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14177            // 'href').set` read `undefined` for a setter that runs.
14178            let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14179                || crate::stdlib::instance_accessors(&ctor)
14180                    .0
14181                    .iter()
14182                    .any(|(k, settable)| *k == key && *settable);
14183            let setter = writable
14184                .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14185            return Ok(with_host(|h| {
14186                let mut m: IndexMap<String, Value> = IndexMap::new();
14187                m.insert("get".into(), getter);
14188                // `undefined`, not null: a read-only accessor has no setter at
14189                // all, and `JSON.stringify` of the descriptor must drop the key
14190                // rather than report `"set": null`.
14191                m.insert("set".into(), setter.unwrap_or(Value::Undef));
14192                m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14193                m.insert("configurable".into(), Value::Bool(true));
14194                h.new_object(m)
14195            }));
14196        }
14197    }
14198    if let Some(ns) = with_host(|h| match h.get(&obj) {
14199        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14200        _ => None,
14201    }) {
14202        let value = namespace_property(&ns, &key);
14203        if !matches!(value, Value::Undef) {
14204            return Ok(builtin_member_descriptor(&ns, &key, value));
14205        }
14206    }
14207    if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14208        return Ok(with_host(|h| {
14209            let mut m: IndexMap<String, Value> = IndexMap::new();
14210            m.insert("value".into(), value);
14211            m.insert("writable".into(), Value::Bool(attrs.writable));
14212            m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14213            m.insert("configurable".into(), Value::Bool(attrs.configurable));
14214            h.new_object(m)
14215        }));
14216    }
14217    // Accessor descriptor?
14218    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14219        return Ok(with_host(|h| {
14220            let a = h.prop_attrs(&obj, &key);
14221            let mut m: IndexMap<String, Value> = IndexMap::new();
14222            m.insert("get".into(), get.unwrap_or(Value::Undef));
14223            m.insert("set".into(), set.unwrap_or(Value::Undef));
14224            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14225            m.insert("configurable".into(), Value::Bool(a.configurable));
14226            h.new_object(m)
14227        }));
14228    }
14229    let val = with_host(|h| match h.get(&obj) {
14230        // A Buffer's own properties are exactly its byte indices, read out of the
14231        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14232        // that V8 keeps on the prototype, so they own no descriptor.
14233        Some(JsObj::Object(p))
14234            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14235        {
14236            match (
14237                p.get("@@bytes").and_then(|b| h.get(b)),
14238                key.parse::<usize>(),
14239            ) {
14240                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14241                _ => None,
14242            }
14243        }
14244        Some(JsObj::Object(p)) => p.get(&key).cloned(),
14245        // An array's index keys read the elements; `length` is the exotic own
14246        // property; anything else is an ordinary own key in the side table.
14247        Some(JsObj::Array(items)) => match key.parse::<usize>() {
14248            // An ELIDED index owns no property at all, so it has no descriptor.
14249            Ok(i) if h.is_hole(&obj, i) => None,
14250            Ok(i) => items.get(i).cloned(),
14251            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14252            Err(_) => h.fn_prop(&obj, &key),
14253        },
14254        // A function/class own prop lives in the fn-prop side table.
14255        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14256        _ => None,
14257    });
14258    match val {
14259        Some(v) => Ok(with_host(|h| {
14260            let a = h.prop_attrs(&obj, &key);
14261            let mut m: IndexMap<String, Value> = IndexMap::new();
14262            m.insert("value".into(), v);
14263            m.insert("writable".into(), Value::Bool(a.writable));
14264            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14265            m.insert("configurable".into(), Value::Bool(a.configurable));
14266            h.new_object(m)
14267        })),
14268        None => Ok(Value::Undef),
14269    }
14270}
14271
14272/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14273/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14274/// the standard "clone with accessors intact" idiom, so this must agree
14275/// key-for-key with `getOwnPropertyNames`.
14276fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14277    let obj = arg0(&args);
14278    let names = object_keys(vec![obj.clone()], 3)?;
14279    let keys: Vec<String> = with_host(|h| match h.get(&names) {
14280        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14281        _ => Vec::new(),
14282    });
14283    let mut out: IndexMap<String, Value> = IndexMap::new();
14284    for k in keys {
14285        let ks = with_host(|h| h.new_str(k.clone()));
14286        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14287        if !matches!(d, Value::Undef) {
14288            out.insert(k, d);
14289        }
14290    }
14291    Ok(with_host(|h| h.new_object(out)))
14292}
14293
14294/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14295/// Proxy's `has` trap is user code and may throw.
14296pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14297    if let Some(b) = crate::proxy::has(obj, key)? {
14298        return Ok(b);
14299    }
14300    Ok(has_property_ordinary(obj, key))
14301}
14302
14303/// `[[HasProperty]]` for every non-Proxy receiver.
14304fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14305    // `key in globalThis`: membership matches what the READ answers, which for
14306    // the global object includes every lazily-bound builtin and every global a
14307    // script created. `'Math' in globalThis` and `'x' in globalThis` after
14308    // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14309    // read back fine.
14310    if with_host(|h| h.is_global_object(obj))
14311        && !CJS_WRAPPER_LOCALS.contains(&key)
14312        && global_object_binding(key).is_some()
14313    {
14314        return true;
14315    }
14316    // `key in <builtin namespace/prototype>`: membership matches what a property
14317    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14318    // prototype methods) resolve as callable thunks via `namespace_property`, so
14319    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14320    // with the `in` operator before reading the intrinsic).
14321    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14322        return !matches!(namespace_property(&ns, key), Value::Undef);
14323    }
14324    // An integer index of a typed array / Buffer is an own property, and lives
14325    // in the hidden element array rather than the property map — the same
14326    // question `hasOwnProperty` answers, through the same helper. Only a hit
14327    // short-circuits: a non-index key like `'length'` must still fall through
14328    // to the ordinary chain lookup below.
14329    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14330        return true;
14331    }
14332    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14333        return true;
14334    }
14335    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14336        return true;
14337    }
14338    // A member patched onto the receiver's intrinsic prototype. The READ
14339    // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14340    // function while `'at' in []` stayed false.
14341    if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14342        return true;
14343    }
14344    if with_host(|h| match h.get(obj) {
14345        Some(JsObj::Object(p)) => p.contains_key(key),
14346        Some(JsObj::Array(items)) => {
14347            key == "length"
14348                || key
14349                    .parse::<usize>()
14350                    .map(|i| i < items.len() && !h.is_hole(obj, i))
14351                    .unwrap_or(false)
14352                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14353                // side table, and `in` must see it.
14354                || h.fn_prop(obj, key).is_some()
14355        }
14356        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14357        // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14358        // the `RegExpObj` struct rather than a property map, so nothing above
14359        // can see it.
14360        Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14361        _ => false,
14362    }) {
14363        return true;
14364    }
14365    // An INHERITED builtin prototype method. These are not objects on the
14366    // prototype chain — they are synthesized by the read path from the
14367    // intrinsic table — so neither `lookup_chain` nor the property map above
14368    // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14369    // Promise.resolve()` all answered false. That last one is the standard
14370    // thenable test, so the `in` operator disagreed with what a read gives for
14371    // every builtin method of every builtin kind.
14372    inherited_builtin_method(obj, key)
14373}
14374
14375/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14376/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14377/// performs no read, so a getter cannot fire.
14378/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14379/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14380/// `None`.
14381///
14382/// The intrinsic prototypes are namespace handles rather than real objects on
14383/// the chain, so an assignment onto one lands in `builtin_statics` and no
14384/// ordinary chain walk can see it. This is the read side: the receiver's own
14385/// constructor's prototype first, then `Object.prototype`, mirroring
14386/// `inherited_method_owner`'s two-step.
14387pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14388    if with_host(|h| h.has_null_proto(obj)) {
14389        return None;
14390    }
14391    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14392        Some(c) => Some(c),
14393        None if is_arguments(obj) => Some("Object"),
14394        None => with_host(|h| default_ctor_name(h, obj)),
14395    };
14396    // Only the side table is consulted, never the real prototype OBJECT's map:
14397    // `String.prototype` and friends are materialized with their intrinsic
14398    // members present, so reading their maps here would re-route every ordinary
14399    // `"a".toString()` through this path — which recursed until the stack blew.
14400    // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14401    // table precisely so the read side can stay this narrow.
14402    let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14403    let found = ctor.and_then(on).or_else(|| on("Object"))?;
14404    // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14405    // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14406    // name back into the table. Dispatching to it would re-enter this lookup
14407    // and recurse until the stack blew, so a thunk that is already this key's
14408    // own intrinsic reports nothing and the ordinary builtin path answers.
14409    let self_thunk = with_host(
14410        |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14411    );
14412    (!self_thunk).then_some(found)
14413}
14414
14415/// Whether `recv` carries `key` as an OWN property — the guard on
14416/// [`inherited_builtin_static`], since an own property shadows anything
14417/// patched onto a prototype.
14418fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14419    with_host(|h| {
14420        if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14421            return true;
14422        }
14423        match h.get(recv) {
14424            Some(JsObj::Object(p)) => p.contains_key(key),
14425            // An ELIDED index owns nothing — the whole point of a hole is that
14426            // the lookup continues up the chain — so it must not count as a
14427            // shadow here or an inherited value at that index stays invisible.
14428            Some(JsObj::Array(items)) => {
14429                key == "length" || {
14430                    key.parse::<usize>()
14431                        .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14432                }
14433            }
14434            _ => false,
14435        }
14436    })
14437}
14438
14439fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14440    if with_host(|h| h.has_null_proto(obj)) {
14441        return false;
14442    }
14443    if let Some(tag) = crate::stdlib::native_tag(obj) {
14444        if crate::stdlib::instance_has_method(&tag, key) {
14445            return true;
14446        }
14447    }
14448    inherited_method_owner(obj, key).is_some()
14449}
14450
14451/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14452/// whether `Array.prototype`'s methods are still reachable from an array.
14453///
14454/// A builtin's methods are synthesized from the receiver's KIND rather than
14455/// found on a chain, so replacing the prototype could not take them away:
14456/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14457/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14458/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14459/// all still answer, as they do in node.
14460///
14461/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14462/// link at all, so this answers true after one map probe and allocates nothing.
14463pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14464    own_intrinsic_reachable(recv)
14465}
14466
14467fn own_intrinsic_reachable(recv: &Value) -> bool {
14468    // A BOXED primitive needs no special case here: its methods resolve through
14469    // `inherited_method_owner`, which applies the wrapper rule itself.
14470    with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14471}
14472
14473/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14474fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14475    let own = Some(ctor);
14476    let mut cur = recv.clone();
14477    for _ in 0..100 {
14478        let explicit = with_host(|h| h.proto_of(&cur));
14479        let Some(p) = explicit else {
14480            // No explicit link: the implicit prototype is this object's own
14481            // kind's, which is what `recv` is asking about only while `cur` is
14482            // still `recv` itself.
14483            if with_host(|h| h.has_null_proto(&cur)) {
14484                return false;
14485            }
14486            let implicit = with_host(|h| default_ctor_name(h, &cur));
14487            // Every implicit prototype chain ends at `Object.prototype`, so a
14488            // question about `Object` is answered yes by any of them.
14489            return implicit == own || ctor == "Object";
14490        };
14491        if with_host(|h| h.is_null(&p)) {
14492            return false;
14493        }
14494        let hit = with_host(|h| {
14495            own.is_some_and(|c| {
14496                matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14497                    || h.intrinsic_proto_ctor(&p) == Some(c)
14498                    || (c == "Object" && h.object_proto() == p)
14499            })
14500        });
14501        if hit {
14502            return true;
14503        }
14504        // A CLASS prototype object is not linked to the builtin its class
14505        // extends — the `extends` relationship is recorded on the class value —
14506        // so the walk has to cross over there or `class D extends Array {}` ends
14507        // it, and every inherited method of every subclass instance vanishes.
14508        if let Some(builtin) = with_host(|h| {
14509            h.class_owning_proto(&p)
14510                .and_then(|c| h.class_builtin_ancestor(&c))
14511                .map(|b| h.callable_name(&b))
14512        }) {
14513            if own == Some(builtin.as_str()) || ctor == "Object" {
14514                return true;
14515            }
14516        }
14517        cur = p;
14518    }
14519    false
14520}
14521
14522/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14523/// — the complement of [`intrinsic_reachable`], which asks about one known
14524/// constructor.
14525///
14526/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14527/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14528/// it: `o.push(1)` works, because those methods are generic over their receiver
14529/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14530/// worked here). Deciding the owner from the receiver's KIND alone made every
14531/// one of them `undefined` — the same "methods come from the kind, not the
14532/// chain" mistake as the detachment case, in the opposite direction.
14533pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14534    chain_intrinsic_ctors(recv)
14535}
14536
14537fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14538    with_host(|h| chain_intrinsic_ctors_h(h, recv))
14539}
14540
14541/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14542/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14543/// back through `with_host` there aborts the process on a double borrow.
14544pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14545    let mut out: Vec<&'static str> = Vec::new();
14546    let mut cur = recv.clone();
14547    for _ in 0..100 {
14548        let Some(p) = h.proto_of(&cur) else {
14549            break;
14550        };
14551        if h.is_null(&p) {
14552            break;
14553        }
14554        let name = match h.get(&p) {
14555            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14556            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14557        };
14558        if let Some(n) = name {
14559            if let Some(c) = crate::arity::PROTO_MEMBERS
14560                .iter()
14561                .map(|(k, _)| *k)
14562                .find(|k| *k == n)
14563            {
14564                if !out.contains(&c) {
14565                    out.push(c);
14566                }
14567            }
14568        }
14569        cur = p;
14570    }
14571    out
14572}
14573
14574/// The constructor whose prototype defines `key` for `obj` — its own if that
14575/// prototype has it, otherwise `Object` — or `None` when neither does.
14576///
14577/// Used both by `in` and by the READ, so the two cannot disagree about which
14578/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14579/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14580pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14581    inherited_method_owner(obj, key)
14582}
14583
14584fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14585    if with_host(|h| h.has_null_proto(obj)) {
14586        return None;
14587    }
14588    // The generated prototype-member table, which unlike the arity table knows
14589    // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14590    // on a Symbol are members but not functions — and about `constructor`.
14591    // A BOXED primitive reports its wrapper's constructor, not `Object` —
14592    // `'description' in Object(Symbol())` is true. The box is an ordinary
14593    // object carrying the primitive in a slot, so the ctor comes from what it
14594    // holds rather than from the box itself.
14595    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14596        Some(c) => Some(c),
14597        // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14598        // spread and `for-of` work, but node's is an exotic that inherits from
14599        // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14600        // its backing kind would hand it the whole `Array.prototype`.
14601        None if is_arguments(obj) => Some("Object"),
14602        None => with_host(|h| default_ctor_name(h, obj)),
14603    };
14604    let on_proto = |c: &str| {
14605        crate::arity::PROTO_MEMBERS
14606            .binary_search_by(|(k, _)| (*k).cmp(c))
14607            .ok()
14608            .is_some_and(|i| {
14609                crate::arity::PROTO_MEMBERS[i]
14610                    .1
14611                    .iter()
14612                    .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14613            })
14614    };
14615    // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14616    // well-known symbol member is absent from it. For an object whose CHAIN
14617    // reaches an intrinsic prototype the intrinsic table has to be consulted as
14618    // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14619    // at all. It is deliberately NOT consulted for the receiver's own kind:
14620    // there a thunk would be minted for every `@@` member the table names,
14621    // including ones whose dispatch has no implementation for that receiver,
14622    // and `[...buffer]` then failed with `@@iterator is not a function`.
14623    //
14624    // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14625    // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14626    // and minting a thunk there produced `@@iterator is not a function` for
14627    // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14628    // fast path, which the table entry would shadow.
14629    let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14630        && crate::stdlib::native_tag(obj).is_none();
14631    let on_proto_or_symbol =
14632        |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14633    // Each candidate is only an answer while ITS prototype is still on the
14634    // receiver's chain. The two are asked separately: replacing an array's
14635    // prototype with a plain object takes `Array.prototype`'s methods away and
14636    // leaves `Object.prototype`'s, since the replacement inherits from it.
14637    if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14638        return Some(c);
14639    }
14640    // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14641    // kind does not account for.
14642    if let Some(c) = chain_intrinsic_ctors(obj)
14643        .into_iter()
14644        .find(|c| on_proto_or_symbol(c))
14645    {
14646        return Some(c);
14647    }
14648    // Everything else inherits `Object.prototype`'s.
14649    if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14650        return Some("Object");
14651    }
14652    None
14653}
14654
14655/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14656/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14657/// that preserves the *reference graph*. Two properties pointing at the same
14658/// object clone to two properties pointing at the same clone, and a cycle clones
14659/// to a cycle instead of recursing forever. `seen` maps each source heap index
14660/// to its clone, which is what buys both.
14661/// The rendering node puts in a `DataCloneError` for a value the structured
14662/// clone algorithm refuses, or `None` when the value IS cloneable.
14663///
14664/// Refusing at all is the point: these used to be copied through by reference,
14665/// so `structuredClone({f: () => 1})` handed back an object sharing the
14666/// original's function and `structuredClone(new WeakMap())` returned the very
14667/// same WeakMap. Node throws on every one of them.
14668fn clone_refusal(v: &Value) -> Option<String> {
14669    let kind = with_host(|h| h.kind_of(v))?;
14670    let render = |ctor: &str| Some(format!("#<{ctor}>"));
14671    match kind {
14672        // A function renders as its SOURCE TEXT here, which each FuncDef
14673        // keeps as a span into its script (`JsHost::func_source`).
14674        ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14675            Some(with_host(|h| h.str_of(v)))
14676        }
14677        ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14678            Some(with_host(|h| h.str_of(v)))
14679        }
14680        ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14681        ObjKind::Promise => render("Promise"),
14682        ObjKind::Generator => Some("[object Generator]".to_string()),
14683        // A proxy is refused by its TARGET's shape: a callable one renders like
14684        // the function it wraps, everything else as a plain object.
14685        ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14686            with_host(|h| h.str_of(v))
14687        } else {
14688            "#<Object>".to_string()
14689        }),
14690        ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14691            render("WeakMap")
14692        }
14693        ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14694            render("WeakSet")
14695        }
14696        _ => match crate::stdlib::native_tag(v).as_deref() {
14697            Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14698            _ => None,
14699        },
14700    }
14701}
14702
14703/// `structuredClone(value[, { transfer }])`.
14704///
14705/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14706/// after the clone — its bytes belong to the copy. The option used to be
14707/// ignored entirely, so the source buffer stayed usable where node leaves it
14708/// with zero length.
14709fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14710    let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14711        Some(opts) => {
14712            let t = get_property(opts, "transfer")?;
14713            if matches!(t, Value::Undef) {
14714                Vec::new()
14715            } else {
14716                host::iter_all(&t)?
14717            }
14718        }
14719        None => Vec::new(),
14720    };
14721    for item in &list {
14722        if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14723            return Err(host::dom_error(
14724                "DataCloneError",
14725                "Found invalid value in transferList.",
14726            ));
14727        }
14728    }
14729    let out = deep_clone(&arg0(&args))?;
14730    for item in &list {
14731        crate::stdlib::typedarray::detach_buffer(item);
14732    }
14733    Ok(out)
14734}
14735
14736pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14737    deep_clone_seen(v, &mut std::collections::HashMap::new())
14738}
14739
14740fn deep_clone_seen(
14741    v: &Value,
14742    seen: &mut std::collections::HashMap<u32, Value>,
14743) -> Result<Value, String> {
14744    let idx = match v {
14745        Value::Obj(i) => *i,
14746        _ => return Ok(v.clone()),
14747    };
14748    if let Some(done) = seen.get(&idx) {
14749        return Ok(done.clone());
14750    }
14751    if crate::stdlib::typedarray::is_detached(v) {
14752        return Err(host::dom_error(
14753            "DataCloneError",
14754            "An ArrayBuffer is detached and could not be cloned.",
14755        ));
14756    }
14757    if let Some(render) = clone_refusal(v) {
14758        return Err(host::dom_error(
14759            "DataCloneError",
14760            &format!("{render} could not be cloned."),
14761        ));
14762    }
14763    // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14764    // handing back the same object let a write through the clone move the
14765    // original's match cursor.
14766    if let Some((src, flags)) = with_host(|h| match h.get(v) {
14767        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14768        _ => None,
14769    }) {
14770        let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14771        let out = regexp_ctor(&args)?;
14772        seen.insert(idx, out.clone());
14773        return Ok(out);
14774    }
14775    Ok(match with_host(|h| h.get(v).cloned()) {
14776        Some(JsObj::Array(items)) => {
14777            // Register the (empty) clone BEFORE recursing so a self-reference
14778            // resolves to it.
14779            let out = with_host(|h| h.new_array(Vec::new()));
14780            seen.insert(idx, out.clone());
14781            let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14782            for x in &items {
14783                cloned.push(deep_clone_seen(x, seen)?);
14784            }
14785            with_host(|h| {
14786                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14787                    *a = cloned;
14788                }
14789                // A sparse source clones to an equally sparse array: the clone
14790                // walks own properties, so a hole is nothing to copy.
14791                h.copy_holes(v, &out, Some);
14792            });
14793            out
14794        }
14795        Some(JsObj::Object(_)) => {
14796            let out = with_host(|h| h.new_object(IndexMap::new()));
14797            seen.insert(idx, out.clone());
14798            // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14799            // walked the property map, where an accessor stores nothing, so
14800            // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14801            // key and a non-enumerable one are dropped, as node drops them.
14802            let is_error = with_host(|h| h.error_to_string(v)).is_some();
14803            let proto = clone_proto(v);
14804            let keeps_proto = !matches!(proto, CloneProto::Plain);
14805            // An ERROR clones its name, message and stack and NOTHING else —
14806            // node drops any other own property, even an enumerable one.
14807            let keys: Vec<String> = if is_error {
14808                // An ERROR clones its name, message and stack and NOTHING else —
14809                // node drops any other own property, even an enumerable one.
14810                ["name", "message", "stack"]
14811                    .iter()
14812                    .filter(|k| has_property(v, k).unwrap_or(false))
14813                    .map(|k| (*k).to_string())
14814                    .collect()
14815            } else if keeps_proto {
14816                // A preserved exotic keeps EVERY own property, including the
14817                // non-enumerable ones and the internal slots — a Date's time
14818                // value, an ArrayBuffer's `byteLength` and byte store, a typed
14819                // array's view. The enumerable-only walk dropped all of those,
14820                // so a cloned Date read `Invalid Date` and a cloned
14821                // ArrayBuffer had no `byteLength`.
14822                with_host(|h| match h.get(v) {
14823                    Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14824                    _ => Vec::new(),
14825                })
14826            } else {
14827                with_host(|h| h.own_enum_key_names(v))
14828            };
14829            let mut cloned: IndexMap<String, Value> = IndexMap::new();
14830            for k in keys {
14831                // An internal slot is read straight out of the map: it is not a
14832                // property, so a `[[Get]]` would not find it.
14833                let val = if k.starts_with("@@") {
14834                    match with_host(|h| match h.get(v) {
14835                        Some(JsObj::Object(p)) => p.get(&k).cloned(),
14836                        _ => None,
14837                    }) {
14838                        Some(val) => val,
14839                        None => continue,
14840                    }
14841                } else {
14842                    get_property(v, &k)?
14843                };
14844                cloned.insert(k, deep_clone_seen(&val, seen)?);
14845            }
14846            // The prototype survives only for the exotics the algorithm knows —
14847            // a Date, an Error, a typed array, a boxed primitive. A USER class
14848            // instance becomes a plain object, which is what node produces;
14849            // keeping every prototype made `structuredClone(new K())
14850            // instanceof K` true.
14851            with_host(|h| {
14852                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14853                    *p = cloned;
14854                }
14855                match &proto {
14856                    CloneProto::Same => {
14857                        if let Some(p) = h.proto_of(v) {
14858                            h.set_proto(&out, p);
14859                        }
14860                    }
14861                    CloneProto::Ctor(c) => {
14862                        h.ensure_error_protos();
14863                        let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14864                        if let Some(p) = p {
14865                            h.set_proto(&out, p);
14866                        }
14867                        // A Buffer clones to a plain `Uint8Array`, so the native
14868                        // tag has to change with the prototype — left alone,
14869                        // `Buffer.isBuffer` still answered true for the clone.
14870                        // A Buffer clones to a plain `Uint8Array`, so the native
14871                        // tag has to change with the prototype — left alone,
14872                        // `Buffer.isBuffer` answered true for the clone and the
14873                        // brand stayed `[object Object]`. A typed array is
14874                        // tagged `TypedArray` and names its element type in
14875                        // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14876                        if c == "Uint8Array" {
14877                            let tag = h.new_str("TypedArray");
14878                            let kind = h.new_str("Uint8Array");
14879                            if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14880                                p.insert("@@native".into(), tag);
14881                                p.insert("@@kind".into(), kind);
14882                            }
14883                        }
14884                    }
14885                    CloneProto::Plain => {}
14886                }
14887                h.copy_prop_attrs(v, &out);
14888            });
14889            out
14890        }
14891        // Map/Set are structured types: clone the entries, keep the kind.
14892        Some(JsObj::Map { entries, weak }) => {
14893            let out = with_host(|h| {
14894                h.alloc(JsObj::Map {
14895                    entries: IndexMap::new(),
14896                    weak,
14897                })
14898            });
14899            seen.insert(idx, out.clone());
14900            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14901            for (k, val) in pairs {
14902                let ck = deep_clone_seen(&k, seen)?;
14903                let cv = deep_clone_seen(&val, seen)?;
14904                let _ = map_method(&out, "set", vec![ck, cv]);
14905            }
14906            out
14907        }
14908        Some(JsObj::Set { entries, weak }) => {
14909            let out = with_host(|h| {
14910                h.alloc(JsObj::Set {
14911                    entries: IndexMap::new(),
14912                    weak,
14913                })
14914            });
14915            seen.insert(idx, out.clone());
14916            let vals: Vec<Value> = entries.values().cloned().collect();
14917            for x in vals {
14918                let cx = deep_clone_seen(&x, seen)?;
14919                let _ = set_method(&out, "add", vec![cx]);
14920            }
14921            out
14922        }
14923        // A string, a BigInt and a boxed primitive are immutable enough to
14924        // share; anything left is a value type.
14925        _ => v.clone(),
14926    })
14927}
14928
14929/// Whether a cloned object keeps the source's prototype.
14930///
14931/// The structured clone algorithm reproduces the exotics it knows and turns
14932/// everything else into a plain object — so a `Date` clones to a `Date` and a
14933/// user class instance clones to an `Object`.
14934fn clone_proto(v: &Value) -> CloneProto {
14935    // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14936    // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14937    // `Error`, not `E`.
14938    if with_host(|h| h.error_to_string(v)).is_some() {
14939        let name = get_property(v, "name")
14940            .map(|n| with_host(|h| h.str_of(&n)))
14941            .unwrap_or_else(|_| "Error".into());
14942        let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14943            name
14944        } else {
14945            "Error".to_string()
14946        };
14947        return CloneProto::Ctor(class);
14948    }
14949    match crate::stdlib::native_tag(v).as_deref() {
14950        // A Buffer is not reproduced as a Buffer: node hands back a plain
14951        // `Uint8Array` over the same bytes.
14952        Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14953        Some(_) => CloneProto::Same,
14954        // A boxed primitive keeps its wrapper; anything else — a user class
14955        // instance included — becomes a plain object.
14956        None if wrapped_primitive(v).is_some() => CloneProto::Same,
14957        None => CloneProto::Plain,
14958    }
14959}
14960
14961/// Which prototype a clone gets: the source's, a named builtin's, or none.
14962enum CloneProto {
14963    Same,
14964    Ctor(String),
14965    Plain,
14966}
14967
14968// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14969
14970/// A short `Name: message` string for an error value (used when an await
14971/// rejection unwinds as a thrown error).
14972pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14973    if let Some(JsObj::Object(props)) = h.get(v) {
14974        let name = props
14975            .get("name")
14976            .map(|x| h.str_of(x))
14977            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14978            .unwrap_or_else(|| "Error".into());
14979        if let Some(m) = props.get("message") {
14980            return format!("{name}: {}", h.str_of(m));
14981        }
14982        return name;
14983    }
14984    h.str_of(v)
14985}
14986
14987/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14988/// value)`, or `() => { throw reason }` on the reject path.
14989///
14990/// Returning the carried value directly — what this used to do — skipped both
14991/// halves. A promise returned by the callback was never awaited, so the
14992/// ordinary async-cleanup shape
14993///
14994/// ```text
14995/// work().finally(() => closeConnection()).then(next)
14996/// ```
14997///
14998/// ran `next` before the connection had closed. And the chain settled three
14999/// microtask ticks early, which is observable in ordering against any other
15000/// chain, not just against a timer.
15001///
15002/// A rejection from the callback's own promise wins over the carried value, so
15003/// no reject handler is attached here: it propagates on its own.
15004fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
15005    // PromiseResolve (27.2.4.7) returns an argument that is already a promise
15006    // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
15007    // thenable takes to adopt it, which showed up as a callback returning a
15008    // rejected promise settling one tick late against every other chain.
15009    let p = match with_host(|h| h.promise_id(&result)) {
15010        Some(_) => result,
15011        None => {
15012            let fresh = with_host(|h| h.new_promise());
15013            if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
15014                host::resolve_promise_val(pid, result);
15015            }
15016            fresh
15017        }
15018    };
15019    let cell = with_host(|h| h.new_array(vec![carried]));
15020    let idx = match cell {
15021        Value::Obj(i) => i,
15022        _ => 0,
15023    };
15024    let tag = if rethrow { "finrethrow" } else { "finret" };
15025    let thunk = make_builtin(format!("@@{tag}:{idx}"));
15026    host::promise_then(&p, thunk, Value::Undef)
15027}
15028
15029fn make_builtin(name: String) -> Value {
15030    with_host(|h| h.alloc(JsObj::Builtin(name)))
15031}
15032
15033/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
15034/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
15035///
15036/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
15037/// an EXPLICIT link, so an object on the default prototype reported `null`:
15038/// `({}).__proto__ === Object.prototype` was false while
15039/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
15040/// the three cannot drift apart again.
15041pub fn prototype_of(v: &Value) -> Value {
15042    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
15043    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
15044    // not `Function.prototype`. This is the class-side half of the subclass
15045    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
15046    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
15047        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
15048    }
15049    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
15050    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
15051    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
15052    // `ClassVal.parent`, but the link itself was invisible, so
15053    // `Object.getPrototypeOf(B) === A` read false and any library walking the
15054    // constructor chain — rather than calling a static — saw a base class.
15055    // A base class keeps the default answer below (`Function.prototype`).
15056    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
15057        if let Some(parent) = c.parent {
15058            return parent;
15059        }
15060    }
15061    // `Object.create(null)` and friends really do have a null prototype.
15062    if with_host(|h| h.has_null_proto(v)) {
15063        return with_host(|h| h.null());
15064    }
15065    // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
15066    // reported itself, because the fallback below answers by constructor name
15067    // and a plain object's is `Object` — an infinite chain to anything walking
15068    // it.
15069    if with_host(|h| h.strict_eq(v, &h.object_proto())) {
15070        return with_host(|h| h.null());
15071    }
15072    // Every OTHER builtin prototype namespace (`Array.prototype`,
15073    // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15074    // would send it back to a namespace handle for its own constructor.
15075    if matches!(
15076        with_host(|h| h.get(v).cloned()),
15077        Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15078    ) {
15079        return with_host(|h| h.object_proto());
15080    }
15081    if let Some(p) = with_host(|h| h.proto_of(v)) {
15082        return p;
15083    }
15084    // A builtin exotic with no explicit `[[Prototype]]` link reports its
15085    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15086    // Array.prototype`), which `strict_eq` compares by name. A plain object
15087    // reports the one real `Object.prototype` object.
15088    with_host(|h| {
15089        h.ensure_native_protos();
15090        match default_ctor_name(h, v) {
15091            Some("Object") => h.object_proto(),
15092            // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15093            // primitive must report that object and not a fresh namespace
15094            // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15095            // compares a thunk against the real object and reads false.
15096            Some(c) => h
15097                .native_proto(c)
15098                .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15099            None => h.null(),
15100        }
15101    })
15102}
15103
15104/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15105/// internal resolve/reject functions.
15106/// A fresh promise built through the SPECIES constructor, when a `Promise`
15107/// static was reached through a subclass.
15108///
15109/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15110/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15111/// plain promise, so nothing a subclass produced was an instance of it. The
15112/// executor is a no-op: the result is settled through its promise id, which is
15113/// what the ordinary path does too.
15114fn promise_species_create() -> Result<Option<Value>, String> {
15115    let Some(ctor) = host::current_static_this() else {
15116        return Ok(None);
15117    };
15118    if !matches!(
15119        with_host(|h| h.kind_of(&ctor)),
15120        Some(ObjKind::Class) | Some(ObjKind::Func)
15121    ) {
15122        return Ok(None);
15123    }
15124    let species = match get_property(&ctor, "@@species") {
15125        Ok(Value::Undef) => ctor,
15126        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15127        Ok(s) => s,
15128        Err(_) => ctor,
15129    };
15130    if !matches!(
15131        with_host(|h| h.kind_of(&species)),
15132        Some(ObjKind::Class) | Some(ObjKind::Func)
15133    ) {
15134        return Ok(None);
15135    }
15136    let noop = make_builtin("@@pnoop".to_string());
15137    let p = host::construct(&species, vec![noop])?;
15138    // Only usable if the subclass really produced a promise; a constructor that
15139    // returned something else has no id to settle.
15140    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15141}
15142
15143/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15144/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15145///
15146/// Distinct from `promise_species_create`, which answers for a STATIC reached
15147/// through a subclass. Here the subclass comes from the receiver itself, so
15148/// `P.resolve(1).then(f)` is also a `P`.
15149pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15150    // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15151    // which has no `constructor` entry of its own.
15152    let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15153    if !matches!(
15154        with_host(|h| h.kind_of(&ctor)),
15155        Some(ObjKind::Class) | Some(ObjKind::Func)
15156    ) {
15157        return Ok(None);
15158    }
15159    let species = match get_property(&ctor, "@@species") {
15160        Ok(Value::Undef) => ctor,
15161        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15162        Ok(s) => s,
15163        Err(_) => ctor,
15164    };
15165    if !matches!(
15166        with_host(|h| h.kind_of(&species)),
15167        Some(ObjKind::Class) | Some(ObjKind::Func)
15168    ) {
15169        return Ok(None);
15170    }
15171    let noop = make_builtin("@@pnoop".to_string());
15172    let p = host::construct(&species, vec![noop])?;
15173    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15174}
15175
15176fn new_promise(executor: Value) -> Result<Value, String> {
15177    let p = with_host(|h| h.new_promise());
15178    let id = with_host(|h| h.promise_id(&p).unwrap());
15179    let res = make_builtin(format!("@@presolve:{id}"));
15180    let rej = make_builtin(format!("@@preject:{id}"));
15181    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15182        // A throw in the executor rejects the promise.
15183        let ev = host::take_exc_or_error(&e);
15184        host::reject_promise_val(id, ev);
15185    }
15186    Ok(p)
15187}
15188
15189/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15190/// already-settled promise.
15191pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15192    promise_resolve(v)
15193}
15194
15195fn promise_resolve(v: Value) -> Result<Value, String> {
15196    if let Some(p) = promise_species_create()? {
15197        let id = with_host(|h| h.promise_id(&p).unwrap());
15198        host::resolve_promise_val(id, v);
15199        return Ok(p);
15200    }
15201    Ok(host::promise_of(&v))
15202}
15203fn promise_reject(v: Value) -> Result<Value, String> {
15204    let p = match promise_species_create()? {
15205        Some(p) => p,
15206        None => with_host(|h| h.new_promise()),
15207    };
15208    let id = with_host(|h| h.promise_id(&p).unwrap());
15209    host::reject_promise_val(id, v);
15210    Ok(p)
15211}
15212
15213/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15214/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15215/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15216/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15217/// callers that hand the resolver to an event listener.
15218pub fn pending_promise_with_resolver() -> (Value, Value) {
15219    let p = with_host(|h| h.new_promise());
15220    let id = with_host(|h| h.promise_id(&p).unwrap());
15221    let resolve = make_builtin(format!("@@presolve:{id}"));
15222    (p, resolve)
15223}
15224
15225/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15226///
15227/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15228/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15229/// without the two running together, and it escapes the punctuation that is
15230/// meaningful inside a character class or a group name.
15231fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15232    let v = arg0(&args);
15233    if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15234        return Err(host::type_error("input argument must be a string"));
15235    }
15236    let s = with_host(|h| h.str_of(&v));
15237    // Punctuation that is escaped by CODE POINT rather than with a backslash.
15238    // Measured against node over the whole ASCII range, not taken from a list:
15239    // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15240    const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15241    const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15242    let mut out = String::with_capacity(s.len());
15243    for (i, c) in s.chars().enumerate() {
15244        // A leading ASCII alphanumeric, and only a leading one.
15245        if i == 0 && c.is_ascii_alphanumeric() {
15246            out.push_str(&format!("\\x{:02x}", c as u32));
15247            continue;
15248        }
15249        if SYNTAX.contains(c) {
15250            out.push('\\');
15251            out.push(c);
15252            continue;
15253        }
15254        match c {
15255            '\t' => out.push_str("\\t"),
15256            '\n' => out.push_str("\\n"),
15257            '\u{b}' => out.push_str("\\v"),
15258            '\u{c}' => out.push_str("\\f"),
15259            '\r' => out.push_str("\\r"),
15260            _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15261                let n = c as u32;
15262                if n <= 0xff {
15263                    out.push_str(&format!("\\x{n:02x}"));
15264                } else {
15265                    out.push_str(&format!("\\u{n:04x}"));
15266                }
15267            }
15268            _ => out.push(c),
15269        }
15270    }
15271    Ok(with_host(|h| h.new_str(out)))
15272}
15273
15274/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15275/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15276/// Unicode but not to ECMAScript, and node leaves both alone.
15277fn is_regex_escape_space(c: char) -> bool {
15278    matches!(
15279        c,
15280        '\u{a0}' | '\u{1680}' | '\u{2000}'
15281            ..='\u{200a}'
15282                | '\u{2028}'
15283                | '\u{2029}'
15284                | '\u{202f}'
15285                | '\u{205f}'
15286                | '\u{3000}'
15287                | '\u{feff}'
15288    )
15289}
15290
15291/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15292/// object that merely INHERITS from `Error.prototype` is not one.
15293fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15294    let v = arg0(&args);
15295    Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15296}
15297
15298/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15299/// and `Object.prototype.toString`'s step 9 both test.
15300///
15301/// The brand is the OWN `stack` an error is built with (a `DOMException`
15302/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15303/// neither, which is why inheriting from an error prototype does not make a
15304/// value an error. Shared so the two cannot disagree — branding by a chain
15305/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15306/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15307/// on the same value already said false.
15308pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15309    match h.get(v) {
15310        Some(JsObj::Object(p)) => {
15311            p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15312        }
15313        _ => false,
15314    }
15315}
15316
15317/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15318/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15319/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15320/// costs a tick that this does not.
15321fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15322    let f = arg0(&args);
15323    // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15324    // returns a rejected promise, so the surrounding `try` never sees it.
15325    if !with_host(|h| host::is_callable(h, &f)) {
15326        // Node names the TYPE alongside the value — `number 5 is not a
15327        // function` — which the ordinary call-site message does not. A plain
15328        // object and a symbol name only the type; `null` names both.
15329        let shown = with_host(|h| {
15330            let kind = h.type_of(&f);
15331            match kind {
15332                "undefined" => "undefined".to_string(),
15333                "symbol" | "bigint" => kind.to_string(),
15334                "object" if h.is_null(&f) => "object null".to_string(),
15335                "object" => "object".to_string(),
15336                "string" => format!("string \"{}\"", h.str_of(&f)),
15337                _ => format!("{kind} {}", h.str_of(&f)),
15338            }
15339        });
15340        let p = with_host(|h| h.new_promise());
15341        let id = with_host(|h| h.promise_id(&p).unwrap());
15342        let reject = make_builtin(format!("@@preject:{id}"));
15343        let err =
15344            with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15345        host::invoke(&reject, vec![err], None)?;
15346        return Ok(p);
15347    }
15348    let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15349    let p = with_host(|h| h.new_promise());
15350    let id = with_host(|h| h.promise_id(&p).unwrap());
15351    let resolve = make_builtin(format!("@@presolve:{id}"));
15352    let reject = make_builtin(format!("@@preject:{id}"));
15353    let promise = p;
15354    match host::invoke(&f, rest, None) {
15355        Ok(v) => {
15356            host::invoke(&resolve, vec![v], None)?;
15357        }
15358        Err(e) => {
15359            // The thrown VALUE, not a re-synthesis of its rendering: a callback
15360            // that throws a `TypeError` must reject with that object, and
15361            // rebuilding it from the message string flattened it to a plain
15362            // `Error` whose message was the rendered `Uncaught TypeError: t`.
15363            let err =
15364                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15365            with_host(|h| {
15366                h.error = None;
15367                h.exc = None;
15368            });
15369            host::invoke(&reject, vec![err], None)?;
15370        }
15371    }
15372    Ok(promise)
15373}
15374
15375fn promise_with_resolvers() -> Result<Value, String> {
15376    let p = with_host(|h| h.new_promise());
15377    let id = with_host(|h| h.promise_id(&p).unwrap());
15378    let resolve = make_builtin(format!("@@presolve:{id}"));
15379    let reject = make_builtin(format!("@@preject:{id}"));
15380    let mut props: IndexMap<String, Value> = IndexMap::new();
15381    props.insert("promise".into(), p);
15382    props.insert("resolve".into(), resolve);
15383    props.insert("reject".into(), reject);
15384    Ok(with_host(|h| h.new_object(props)))
15385}
15386
15387/// A promise already rejected with `e` — what every combinator hands back when
15388/// the ITERABLE misbehaves.
15389///
15390/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15391/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15392/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15393/// and the caller saw the error at the call site instead.
15394fn rejected_promise(e: String) -> Value {
15395    let p = with_host(|h| h.new_promise());
15396    let id = with_host(|h| h.promise_id(&p).unwrap());
15397    let reject = make_builtin(format!("@@preject:{id}"));
15398    let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15399    with_host(|h| {
15400        h.error = None;
15401        h.exc = None;
15402    });
15403    let _ = host::invoke(&reject, vec![err], None);
15404    p
15405}
15406
15407#[derive(Clone, Copy)]
15408enum AllMode {
15409    All,
15410    AllSettled,
15411}
15412
15413/// `Promise.all` / `Promise.allSettled`.
15414fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15415    let items = match host::iter_all(&arg0(&args)) {
15416        Ok(v) => v,
15417        Err(e) => return Ok(rejected_promise(e)),
15418    };
15419    // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15420    // subclass the promise it hands back is an instance of that subclass.
15421    let result = match promise_species_create()? {
15422        Some(p) => p,
15423        None => with_host(|h| h.new_promise()),
15424    };
15425    let rid = with_host(|h| h.promise_id(&result).unwrap());
15426    let n = items.len();
15427    if n == 0 {
15428        let empty = with_host(|h| h.new_array(Vec::new()));
15429        host::resolve_promise_val(rid, empty);
15430        return Ok(result);
15431    }
15432    // Shared mutable accumulator via Rc<RefCell<…>>.
15433    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15434    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15435    for (i, it) in items.into_iter().enumerate() {
15436        let ap = host::promise_of(&it);
15437        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15438        let slots = slots.clone();
15439        let remaining = remaining.clone();
15440        host::subscribe_native(
15441            aid,
15442            Box::new(move |state, val| {
15443                let settled = match mode {
15444                    AllMode::All => {
15445                        if state == host::PromiseState::Rejected {
15446                            host::reject_promise_val(rid, val);
15447                            return Ok(());
15448                        }
15449                        val
15450                    }
15451                    AllMode::AllSettled => with_host(|h| {
15452                        let mut m: IndexMap<String, Value> = IndexMap::new();
15453                        if state == host::PromiseState::Rejected {
15454                            m.insert("status".into(), h.new_str("rejected"));
15455                            m.insert("reason".into(), val);
15456                        } else {
15457                            m.insert("status".into(), h.new_str("fulfilled"));
15458                            m.insert("value".into(), val);
15459                        }
15460                        h.new_object(m)
15461                    }),
15462                };
15463                slots.borrow_mut()[i] = settled;
15464                let mut r = remaining.borrow_mut();
15465                *r -= 1;
15466                if *r == 0 {
15467                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15468                    host::resolve_promise_val(rid, arr);
15469                }
15470                Ok(())
15471            }),
15472        );
15473    }
15474    Ok(result)
15475}
15476
15477/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15478fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15479    let items = match host::iter_all(&arg0(&args)) {
15480        Ok(v) => v,
15481        Err(e) => return Ok(rejected_promise(e)),
15482    };
15483    // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15484    let result = match promise_species_create()? {
15485        Some(p) => p,
15486        None => with_host(|h| h.new_promise()),
15487    };
15488    let rid = with_host(|h| h.promise_id(&result).unwrap());
15489    let n = items.len();
15490    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15491    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15492    for (i, it) in items.into_iter().enumerate() {
15493        let ap = host::promise_of(&it);
15494        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15495        let errors = errors.clone();
15496        let remaining = remaining.clone();
15497        host::subscribe_native(
15498            aid,
15499            Box::new(move |state, val| {
15500                if any {
15501                    if state == host::PromiseState::Fulfilled {
15502                        host::resolve_promise_val(rid, val);
15503                    } else {
15504                        errors.borrow_mut()[i] = val;
15505                        let mut r = remaining.borrow_mut();
15506                        *r -= 1;
15507                        if *r == 0 {
15508                            // All rejected → AggregateError carrying every reason.
15509                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15510                            let msg = with_host(|h| h.new_str("All promises were rejected"));
15511                            let agg = make_error_inner("AggregateError", &[reasons, msg]);
15512                            host::reject_promise_val(rid, agg);
15513                        }
15514                    }
15515                } else if state == host::PromiseState::Rejected {
15516                    host::reject_promise_val(rid, val);
15517                } else {
15518                    host::resolve_promise_val(rid, val);
15519                }
15520                Ok(())
15521            }),
15522        );
15523    }
15524    Ok(result)
15525}
15526
15527/// `.then` / `.catch` / `.finally` on a promise.
15528fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15529    match name {
15530        "then" => Ok(host::promise_then(
15531            recv,
15532            args.first().cloned().unwrap_or(Value::Undef),
15533            args.get(1).cloned().unwrap_or(Value::Undef),
15534        )),
15535        "catch" => Ok(host::promise_then(
15536            recv,
15537            Value::Undef,
15538            args.first().cloned().unwrap_or(Value::Undef),
15539        )),
15540        "finally" => {
15541            let cb = arg0(&args);
15542            // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15543            // as BOTH handlers, and `then` ignores a non-callable one — so the
15544            // value or reason simply passes through. Building the thunks
15545            // regardless meant `p.finally(null)` tried to call `null`.
15546            if !with_host(|h| host::is_callable(h, &cb)) {
15547                return Ok(host::promise_then(recv, cb.clone(), cb));
15548            }
15549            let i = match cb {
15550                Value::Obj(i) => i,
15551                _ => 0,
15552            };
15553            let pass = make_builtin(format!("@@finpass:{i}"));
15554            let throw = make_builtin(format!("@@finthrow:{i}"));
15555            Ok(host::promise_then(recv, pass, throw))
15556        }
15557        _ => Err(host::type_error(&format!(
15558            "promise.{name} is not a function"
15559        ))),
15560    }
15561}
15562
15563fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15564    with_host(|h| {
15565        if next_tick {
15566            h.queue_nexttick(cb, args);
15567        } else {
15568            h.queue_micro(cb, args);
15569        }
15570    });
15571}
15572
15573/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15574/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15575/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15576///
15577/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15578/// fires, so it runs until cleared and — being referenced — holds the process
15579/// open exactly as in Node.
15580fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15581    let cb = arg0(&args);
15582    let delay = if name == "setImmediate" {
15583        -1.0 // before any 0ms timeout
15584    } else {
15585        args.get(1)
15586            .map(|d| with_host(|h| h.to_number(d)))
15587            .unwrap_or(0.0)
15588            .max(0.0)
15589    };
15590    let extra = if name == "setImmediate" {
15591        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15592    } else {
15593        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15594    };
15595    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15596    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15597    let interval = (name == "setInterval").then(|| delay.max(1.0));
15598    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15599    let tag = if name == "setImmediate" {
15600        "Immediate"
15601    } else {
15602        "Timeout"
15603    };
15604    crate::stdlib::timers::new_handle(id, tag)
15605}
15606
15607/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15608/// by the bare id it coerces to (code that stored `+timer` still works).
15609fn clear_timer(v: &Value) {
15610    let id =
15611        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15612    with_host(|h| h.cancel_timer(id));
15613}