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| h.alloc(JsObj::Iter { items, idx: 0 })),
5240            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5241            Err(e) => abort(vm, e),
5242        };
5243    }
5244    // Arrays and strings take the direct path below: they have no iterator
5245    // state to preserve and are the hot case, so they must not pay a property
5246    // lookup and a call per loop.
5247    let direct = matches!(
5248        with_host(|h| h.kind_of(&v)),
5249        Some(ObjKind::Array) | Some(ObjKind::Str)
5250    );
5251    // …but only while their `Symbol.iterator` is still reachable. It comes from
5252    // the intrinsic prototype, so replacing the link takes it away: node reports
5253    // `a is not iterable` for an array whose prototype is a plain object, where
5254    // the fast path below iterated the backing vector regardless.
5255    if !own_intrinsic_reachable(&v)
5256        && !matches!(
5257            get_property(&v, "@@iterator"),
5258            Ok(ref f) if with_host(|h| host::is_callable(h, f))
5259        )
5260    {
5261        let shown = with_host(|h| h.inspect(&v));
5262        let msg = host::type_error(&format!("{shown} is not iterable"));
5263        return abort(vm, host::name_call_site(vm, &shown, msg));
5264    }
5265    // Anything else with a `Symbol.iterator`: call it for the iterator object.
5266    //
5267    // Resolved as a full property READ, not a stored-property lookup. A
5268    // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5269    // dispatches its methods through the stdlib table rather than a property
5270    // map, so a `lookup_chain` probe found nothing and the loop fell through to
5271    // materializing the value — which threw for `URLSearchParams` and
5272    // snapshotted for `Map`. Spreading the same object already worked, because
5273    // that path had been fixed and this one had not.
5274    if !direct {
5275        if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5276            if with_host(|h| host::is_callable(h, &iter_fn)) {
5277                return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5278                    Ok(it) => it,
5279                    Err(e) => abort(vm, e),
5280                };
5281            }
5282        }
5283    }
5284    match with_host(|h| h.iter_vec(&v)) {
5285        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5286        // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5287        // reports `a is not iterable`. The text was recorded for this op.
5288        Err(e) => {
5289            let shown = with_host(|h| h.inspect(&v));
5290            let named = host::name_call_site(vm, &shown, e);
5291            abort(vm, named)
5292        }
5293    }
5294}
5295
5296fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5297    let v = vm.pop();
5298    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5299    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5300    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5301    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5302        // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5303        // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5304        // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5305        // the body the way node's do, instead of all firing up front.
5306        return match crate::proxy::own_keys(&v) {
5307            Ok(keys) => with_host(|h| {
5308                let out: Vec<Value> = keys
5309                    .unwrap_or_default()
5310                    .into_iter()
5311                    .filter(|k| !host::is_symbol_key(k))
5312                    .map(|k| h.new_str(k))
5313                    .collect();
5314                h.new_array(out)
5315            }),
5316            Err(e) => abort(vm, e),
5317        };
5318    }
5319    let mut keys = with_host(|h| h.enum_keys(&v));
5320    // A member patched onto the receiver's INTRINSIC prototype is enumerable
5321    // and inherited, so `for-in` visits it after the own keys — but the
5322    // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5323    // pass never reaches them.
5324    if !with_host(|h| h.has_null_proto(&v)) {
5325        let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5326        for ns in intrinsic_proto_namespaces(&v) {
5327            for k in with_host(|h| h.builtin_static_keys(&ns)) {
5328                if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5329                    keys.push(with_host(|h| h.new_str(k)));
5330                }
5331            }
5332        }
5333    }
5334    with_host(|h| h.new_array(keys))
5335}
5336
5337/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5338/// own constructor's and then `Object`'s, the same two steps
5339/// `inherited_builtin_static` looks a value up in.
5340fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5341    let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5342        Some(c) => Some(c),
5343        None if is_arguments(v) => Some("Object"),
5344        None => with_host(|h| default_ctor_name(h, v)),
5345    };
5346    let mut out: Vec<String> = ctor
5347        .filter(|c| *c != "Object")
5348        .map(|c| format!("{c}.prototype"))
5349        .into_iter()
5350        .collect();
5351    out.push("Object.prototype".to_string());
5352    out
5353}
5354
5355/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5356///
5357/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5358/// of the enumerable keys is observationally the same for everything except
5359/// this), and the body can delete a key before the loop reaches it. Node does
5360/// not visit a key deleted that way; without this check `delete d.z` inside the
5361/// loop still produced `x,y,z`.
5362///
5363/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5364/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5365/// `has` trap, which node never fires for `for-in`. That also puts each trap
5366/// call immediately before its visit, matching node's interleaving — the trap
5367/// log used to show every `gopd` up front because the key list was filtered
5368/// eagerly.
5369fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5370    let key = vm.pop();
5371    let obj = vm.pop();
5372    let name = with_host(|h| h.str_of(&key));
5373    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5374        return match crate::proxy::own_enumerable(&obj, &name) {
5375            Ok(b) => Value::Bool(b),
5376            Err(e) => abort(vm, e),
5377        };
5378    }
5379    // A STRING's keys are its character indices. `in` is not defined on a string
5380    // primitive at all, so the ordinary path below has no answer for one and
5381    // `for (const i in 'abc')` came back empty.
5382    if let Some(s) = with_host(|h| h.as_str(&obj)) {
5383        let len = crate::utf16::len(&s);
5384        return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5385    }
5386    // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5387    // there and does NOT re-check enumerability — making one non-enumerable
5388    // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5389    // (The Proxy branch above does re-check, because there the answer comes from
5390    // the trap node itself calls.)
5391    Value::Bool(has_property_ordinary(&obj, &name))
5392}
5393
5394fn b_foriter(vm: &mut VM, _: u8) -> Value {
5395    let it = match vm.stack.last() {
5396        Some(v) => v.clone(),
5397        None => return abort(vm, "internal: FORITER with empty stack".into()),
5398    };
5399    // Eager array-backed iterator (arrays/strings/Map/Set).
5400    let eager = with_host(|h| {
5401        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
5402            if *idx < items.len() {
5403                let v = items[*idx].clone();
5404                *idx += 1;
5405                return Some(Some(v));
5406            }
5407            return Some(None);
5408        }
5409        None
5410    });
5411    if let Some(step) = eager {
5412        return match step {
5413            Some(v) => {
5414                vm.push(v);
5415                Value::Bool(true)
5416            }
5417            None => Value::Bool(false),
5418        };
5419    }
5420    // Generator: resume one step.
5421    if with_host(|h| h.is_generator_val(&it)) {
5422        return match host::gen_resume(&it, Value::Undef) {
5423            Ok(host::GenStep::Yield(v)) => {
5424                vm.push(v);
5425                Value::Bool(true)
5426            }
5427            Ok(host::GenStep::Done(_)) => Value::Bool(false),
5428            Err(e) => abort(vm, e),
5429        };
5430    }
5431    // A user iterator object with a `.next()` returning `{ value, done }`.
5432    match host::call_method(&it, "next", Vec::new()) {
5433        Ok(step) => {
5434            let done = get_property(&step, "done")
5435                .map(|d| with_host(|h| h.truthy(&d)))
5436                .unwrap_or(true);
5437            if done {
5438                Value::Bool(false)
5439            } else {
5440                match get_property(&step, "value") {
5441                    Ok(v) => {
5442                        vm.push(v);
5443                        Value::Bool(true)
5444                    }
5445                    Err(e) => abort(vm, e),
5446                }
5447            }
5448        }
5449        Err(e) => abort(vm, e),
5450    }
5451}
5452
5453fn b_unpack(vm: &mut VM, _: u8) -> Value {
5454    let star = match vm.pop() {
5455        Value::Int(n) => n,
5456        _ => -1,
5457    };
5458    let count = match vm.pop() {
5459        Value::Int(n) => n as usize,
5460        _ => 0,
5461    };
5462    let iterable = vm.pop();
5463    // Without a `...rest` element the pattern needs exactly `count` values and
5464    // must then close the iterator; draining hung on an unbounded source.
5465    let items = match if star < 0 {
5466        host::iter_take(&iterable, count)
5467    } else {
5468        host::iter_all(&iterable)
5469    } {
5470        Ok(v) => v,
5471        // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5472        // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5473        // was recorded for this op at compile time.
5474        Err(e) => {
5475            // Node names the source only when the pattern's right-hand side is
5476            // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5477            // Anything else (a member, a call, a nested pattern, a parameter)
5478            // reports the TYPE instead, with the property note. Measured across
5479            // twelve shapes rather than guessed.
5480            let msg = match host::call_site_text(vm) {
5481                Some(text) => host::type_error(&format!("{text} is not iterable")),
5482                None if e.ends_with(" is not iterable") => {
5483                    host::type_error(&not_iterable_typed(&iterable))
5484                }
5485                None => e,
5486            };
5487            return abort(vm, msg);
5488        }
5489    };
5490    let ordered: Vec<Value> = if star < 0 {
5491        (0..count)
5492            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5493            .collect()
5494    } else {
5495        let si = star as usize;
5496        let after = count.saturating_sub(si + 1);
5497        let rest_end = items.len().saturating_sub(after).max(si);
5498        let mut out: Vec<Value> = Vec::with_capacity(count);
5499        for i in 0..si {
5500            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5501        }
5502        let rest: Vec<Value> = items
5503            .get(si..rest_end)
5504            .map(|s| s.to_vec())
5505            .unwrap_or_default();
5506        out.push(with_host(|h| h.new_array(rest)));
5507        for j in 0..after {
5508            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5509        }
5510        out
5511    };
5512    if ordered.is_empty() {
5513        return Value::Undef;
5514    }
5515    for it in ordered[1..].iter().rev().cloned() {
5516        vm.push(it);
5517    }
5518    ordered[0].clone()
5519}
5520
5521fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5522    let flat = pop_n(vm, argc as usize);
5523    let mut out = Vec::new();
5524    // Elided positions of an array literal (tag 2), recorded as the run-time
5525    // index each lands on — which only this walk knows, because a preceding
5526    // spread contributes an unknown number of elements. Call-argument lists,
5527    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5528    // empty for them.
5529    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5530    let mut i = 0;
5531    while i + 1 < flat.len() {
5532        let val = flat[i + 1].clone();
5533        match flat[i] {
5534            // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5535            // report a non-iterable differently, which is the only reason the
5536            // two are told apart here.
5537            Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5538                let shown = with_host(|h| h.inspect(&val));
5539                host::name_call_site(vm, &shown, e)
5540            }) {
5541                Ok(items) => out.extend(items),
5542                Err(e) => return abort(vm, e),
5543            },
5544            Value::Int(3) => match host::iter_all(&val) {
5545                Ok(items) => out.extend(items),
5546                Err(e) => {
5547                    // A NULLISH spread names the value and what could not be
5548                    // read off it; anything else names the missing protocol.
5549                    let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5550                    return abort(
5551                        vm,
5552                        match shown {
5553                            Some(s) => host::type_error(&format!(
5554                                "{s} is not iterable (cannot read property {s})"
5555                            )),
5556                            None if e.ends_with(" is not iterable") => host::type_error(
5557                                "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5558                            ),
5559                            None => e,
5560                        },
5561                    );
5562                }
5563            },
5564            Value::Int(2) => {
5565                holes.insert(out.len());
5566                out.push(Value::Undef);
5567            }
5568            _ => out.push(val),
5569        }
5570        i += 2;
5571    }
5572    with_host(|h| {
5573        let arr = h.new_array(out);
5574        h.install_holes(&arr, holes);
5575        arr
5576    })
5577}
5578
5579// ── calls ──────────────────────────────────────────────────────────────────────
5580
5581fn b_call(vm: &mut VM, argc: u8) -> Value {
5582    let mut args = pop_n(vm, argc as usize);
5583    let name = sval(&args.remove(0));
5584    let r = host::call_named(&name, args);
5585    // A bare name that resolved to a non-callable reports the VALUE
5586    // (`undefined is not a function`); node names the identifier. Resolving it
5587    // again to learn what the message said costs nothing off the error path.
5588    let r = r.map_err(|e| {
5589        let shown = global_binding(&name)
5590            .map(|v| with_host(|h| h.str_of(&v)))
5591            .unwrap_or_default();
5592        host::name_call_site(vm, &shown, e)
5593    });
5594    finish(vm, r)
5595}
5596
5597/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5598/// method name. `call_method` resolves by name and bottoms out in
5599/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5600/// that happens to be a function reported "is not a function". Read the element
5601/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5602/// A computed call's key is a property key, so it goes through ToPropertyKey:
5603/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5604/// answers "" for a number, which turned `arr[0]()` into a call to the method
5605/// named "" — so the key is stringified here instead.
5606fn call_key_of(v: &Value) -> String {
5607    if let Value::Str(s) = v {
5608        return (**s).clone();
5609    }
5610    // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5611    // spelling that `str_of` does not produce — it renders
5612    // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5613    // method by that display text and reported it was not a function, for every
5614    // object including a plain literal with a computed symbol method. Reading
5615    // the same property without calling it worked, which is what hid this.
5616    with_host(|h| h.property_key(v))
5617}
5618
5619fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5620    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5621        return None;
5622    }
5623    let f = get_property(recv, name).ok()?;
5624    with_host(|h| host::is_callable(h, &f))
5625        .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5626}
5627
5628fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5629    let mut args = pop_n(vm, argc as usize);
5630    let recv = args.remove(0);
5631    let name = call_key_of(&args.remove(0));
5632    if let Some(r) = index_element_call(&recv, &name, &args) {
5633        return finish(vm, r);
5634    }
5635    let r = host::call_method(&recv, &name, args);
5636    // `z.f()` on a missing method is `z.f is not a function` in node, not
5637    // `f is not a function`: V8 names the callee as the source wrote it. The
5638    // text was recorded for this op at compile time.
5639    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5640    finish(vm, r)
5641}
5642
5643fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5644    let mut args = pop_n(vm, argc as usize);
5645    let callable = args.remove(0);
5646    let r = host::invoke(&callable, args, None);
5647    // The callee here is an expression, not a name, so the message it produced
5648    // describes the VALUE (`undefined is not a function`); node names the
5649    // expression. Same site table, keyed on that rendering.
5650    let r = r.map_err(|e| {
5651        let shown = with_host(|h| h.str_of(&callable));
5652        host::name_call_site(vm, &shown, e)
5653    });
5654    finish(vm, r)
5655}
5656
5657/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5658/// rather than a fixed count of stack slots.
5659///
5660/// `compile_new` used to compile each argument with `compile_expr`, and a
5661/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5662/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5663/// Invalid Date.
5664fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5665    let args_arr = vm.pop();
5666    let ctor = vm.pop();
5667    let args = host::iter_all(&args_arr).unwrap_or_default();
5668    let r = host::construct(&ctor, args).map_err(|e| {
5669        let shown = with_host(|h| h.str_of(&ctor));
5670        host::name_call_site(vm, &shown, e)
5671    });
5672    finish(vm, r)
5673}
5674
5675fn b_new(vm: &mut VM, argc: u8) -> Value {
5676    let mut args = pop_n(vm, argc as usize);
5677    let ctor = args.remove(0);
5678    let r = host::construct(&ctor, args);
5679    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5680    // call does.
5681    let r = r.map_err(|e| {
5682        let shown = with_host(|h| h.str_of(&ctor));
5683        host::name_call_site(vm, &shown, e)
5684    });
5685    finish(vm, r)
5686}
5687
5688fn b_apply(vm: &mut VM, _: u8) -> Value {
5689    let args_arr = vm.pop();
5690    let callable = vm.pop();
5691    let args = host::iter_all(&args_arr).unwrap_or_default();
5692    let r = host::invoke(&callable, args, None);
5693    finish(vm, r)
5694}
5695
5696fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5697    let args_arr = vm.pop();
5698    let name = call_key_of(&vm.pop());
5699    let recv = vm.pop();
5700    let args = host::iter_all(&args_arr).unwrap_or_default();
5701    if let Some(r) = index_element_call(&recv, &name, &args) {
5702        return finish(vm, r);
5703    }
5704    let r = host::call_method(&recv, &name, args);
5705    finish(vm, r)
5706}
5707
5708// ── numeric hook ──────────────────────────────────────────────────────────────
5709
5710/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5711/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5712///
5713/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5714/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5715/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5716/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5717/// the VM and `arith` runs under the host's `RefCell` borrow.
5718pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5719    use NumOp::*;
5720    let (a, b) = match op {
5721        // `==`/`!=` only convert when the OTHER side is a primitive that can be
5722        // compared numerically or textually; `{} == {}` stays a reference check.
5723        Eq | Ne => {
5724            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5725            match (pa, pb) {
5726                (false, true) if coerces_against_object(b) => {
5727                    (host::to_primitive(a, "default")?, b.clone())
5728                }
5729                (true, false) if coerces_against_object(a) => {
5730                    (a.clone(), host::to_primitive(b, "default")?)
5731                }
5732                _ => (a.clone(), b.clone()),
5733            }
5734        }
5735        // `+` uses the default hint (`valueOf` first, but a string result still
5736        // selects concatenation); everything else uses the number hint.
5737        Add => (
5738            host::to_primitive(a, "default")?,
5739            host::to_primitive(b, "default")?,
5740        ),
5741        _ => (
5742            host::to_primitive(a, "number")?,
5743            host::to_primitive(b, "number")?,
5744        ),
5745    };
5746    reject_symbol_operand(op, &a, &b)?;
5747    with_host(|h| h.arith(op, &a, &b))
5748}
5749
5750/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5751/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5752/// concatenated `Symbol(desc)` into the result.
5753///
5754/// Which of the two messages V8 uses is decided by whether the operation is
5755/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5756/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5757/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5758/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5759/// (`Symbol() == 1` is `false`), so they are left alone.
5760fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5761    use NumOp::*;
5762    if matches!(op, Eq | Ne) {
5763        return Ok(());
5764    }
5765    let (sym, concat) = with_host(|h| {
5766        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5767        let is_str =
5768            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5769        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5770    });
5771    if !sym {
5772        return Ok(());
5773    }
5774    Err(host::type_error(if matches!(op, Add) && concat {
5775        "Cannot convert a Symbol value to a string"
5776    } else {
5777        "Cannot convert a Symbol value to a number"
5778    }))
5779}
5780
5781/// Whether a primitive `v` makes `==` against an object convert that object
5782/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5783/// `undefined` and booleans are settled without a `ToPrimitive` call
5784/// (a boolean is coerced to a number first, and then it does).
5785fn coerces_against_object(v: &Value) -> bool {
5786    match v {
5787        Value::Undef => false,
5788        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5789        _ => with_host(|h| !h.is_null(v)),
5790    }
5791}
5792
5793// ══ standard library ═══════════════════════════════════════════════════════════
5794
5795/// Namespaces reachable as bare globals.
5796fn is_namespace(name: &str) -> bool {
5797    matches!(
5798        name,
5799        "console"
5800            | "Math"
5801            | "JSON"
5802            | "Object"
5803            | "Array"
5804            | "Number"
5805            | "String"
5806            | "Boolean"
5807            | "Symbol"
5808            | "Reflect"
5809            | "Promise"
5810            | "process"
5811            | "Buffer"
5812            | "URL"
5813            | "URLSearchParams"
5814    )
5815}
5816
5817const GLOBAL_FUNCS: &[&str] = &[
5818    "parseInt",
5819    "parseFloat",
5820    "isNaN",
5821    "isFinite",
5822    "encodeURIComponent",
5823    "decodeURIComponent",
5824    "encodeURI",
5825    "decodeURI",
5826    // Annex B legacy encoders. Still globals on every engine, and still called
5827    // by pre-`encodeURIComponent` library code.
5828    "escape",
5829    "unescape",
5830    "eval",
5831    "String",
5832    "Number",
5833    "Boolean",
5834    "Array",
5835    "Object",
5836    "Function",
5837    "Symbol",
5838    "Map",
5839    "Set",
5840    "WeakMap",
5841    "WeakSet",
5842    "Promise",
5843    "Error",
5844    "TypeError",
5845    "RangeError",
5846    "SyntaxError",
5847    "ReferenceError",
5848    "EvalError",
5849    "URIError",
5850    "AggregateError",
5851    "DOMException",
5852    "Iterator",
5853    "BigInt",
5854    "RegExp",
5855    "Date",
5856    "ArrayBuffer",
5857    "DataView",
5858    "Uint8Array",
5859    "Int8Array",
5860    "Uint8ClampedArray",
5861    "Int16Array",
5862    "Uint16Array",
5863    "Int32Array",
5864    "Uint32Array",
5865    "Float32Array",
5866    "Float64Array",
5867    "BigInt64Array",
5868    "BigUint64Array",
5869    "WeakRef",
5870    "FinalizationRegistry",
5871    "TextEncoder",
5872    "TextDecoder",
5873    // WHATWG Fetch globals (see `stdlib::fetch`).
5874    "fetch",
5875    "Headers",
5876    "Request",
5877    "Response",
5878    "Blob",
5879    "File",
5880    "FormData",
5881    "AbortController",
5882    "AbortSignal",
5883    "queueMicrotask",
5884    "setTimeout",
5885    "setInterval",
5886    "setImmediate",
5887    "clearTimeout",
5888    "clearInterval",
5889    "clearImmediate",
5890    "structuredClone",
5891    // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5892    // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5893    "btoa",
5894    "atob",
5895    "Proxy",
5896    "require",
5897    // CommonJS loader dispatch targets referenced by per-module `require`
5898    // closures (see `module.rs`); never written by user code.
5899    "__cjs_require",
5900    "__cjs_resolve",
5901    "__cjs_cache",
5902];
5903
5904const NS_METHODS: &[&str] = &[
5905    "console.log",
5906    "console.error",
5907    "console.warn",
5908    "console.info",
5909    "console.debug",
5910    "Math.abs",
5911    "Math.acos",
5912    "Math.acosh",
5913    "Math.asin",
5914    "Math.asinh",
5915    "Math.atan",
5916    "Math.atanh",
5917    "Math.atan2",
5918    "Math.ceil",
5919    "Math.cbrt",
5920    "Math.expm1",
5921    "Math.clz32",
5922    "Math.cos",
5923    "Math.cosh",
5924    "Math.exp",
5925    "Math.floor",
5926    "Math.fround",
5927    "Math.hypot",
5928    "Math.imul",
5929    "Math.log",
5930    "Math.log1p",
5931    "Math.log2",
5932    "Math.log10",
5933    "Math.max",
5934    "Math.min",
5935    "Math.pow",
5936    "Math.random",
5937    "Math.round",
5938    "Math.sign",
5939    "Math.sin",
5940    "Math.sinh",
5941    "Math.sqrt",
5942    "Math.tan",
5943    "Math.tanh",
5944    "Math.trunc",
5945    "JSON.stringify",
5946    "JSON.parse",
5947    "JSON.rawJSON",
5948    "JSON.isRawJSON",
5949    "Object.keys",
5950    "Object.values",
5951    "Object.entries",
5952    "Object.assign",
5953    "Object.freeze",
5954    "Object.is",
5955    "Object.fromEntries",
5956    "Object.getPrototypeOf",
5957    "Object.setPrototypeOf",
5958    "Object.create",
5959    "Object.getOwnPropertyNames",
5960    "Object.getOwnPropertySymbols",
5961    "Object.defineProperty",
5962    "Object.getOwnPropertyDescriptor",
5963    "Object.getOwnPropertyDescriptors",
5964    "Object.defineProperties",
5965    "Object.isFrozen",
5966    "Object.isSealed",
5967    "Object.seal",
5968    "Object.preventExtensions",
5969    "Object.isExtensible",
5970    "Object.hasOwn",
5971    "Object.groupBy",
5972    "Array.isArray",
5973    "Array.from",
5974    "Array.fromAsync",
5975    "Array.of",
5976    "Number.isFinite",
5977    "Number.isInteger",
5978    "Number.isNaN",
5979    "Number.isSafeInteger",
5980    "Number.parseFloat",
5981    "Number.parseInt",
5982    "String.fromCharCode",
5983    "String.fromCodePoint",
5984    "String.raw",
5985    "Symbol.for",
5986    "Symbol.keyFor",
5987    "BigInt.asIntN",
5988    "BigInt.asUintN",
5989    "Proxy.revocable",
5990    "Reflect.defineProperty",
5991    "Reflect.deleteProperty",
5992    "Reflect.apply",
5993    "Reflect.construct",
5994    "Reflect.get",
5995    "Reflect.getOwnPropertyDescriptor",
5996    "Reflect.getPrototypeOf",
5997    "Reflect.has",
5998    "Reflect.isExtensible",
5999    "Reflect.ownKeys",
6000    "Reflect.preventExtensions",
6001    "Reflect.set",
6002    "Reflect.setPrototypeOf",
6003    "Promise.resolve",
6004    "Promise.reject",
6005    "Promise.all",
6006    "Promise.allSettled",
6007    "Promise.race",
6008    "Promise.any",
6009    "Promise.withResolvers",
6010    "Promise.try",
6011    "RegExp.escape",
6012    "Error.isError",
6013    "Map.groupBy",
6014    "Response.json",
6015    "Response.error",
6016    "Response.redirect",
6017    "AbortSignal.abort",
6018    "AbortSignal.timeout",
6019    "process.nextTick",
6020    "Error.captureStackTrace",
6021    "require.resolve",
6022    "require.resolve.paths",
6023    "process.memoryUsage.rss",
6024];
6025
6026/// The `name` and `length` a builtin function reports, from the generated
6027/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6028/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6029/// and the core-module functions, whose arity is not specified anywhere.
6030pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6031    crate::arity::BUILTIN_ARITY
6032        .binary_search_by(|(k, _, _)| (*k).cmp(key))
6033        .ok()
6034        .map(|i| {
6035            let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6036            (name, len)
6037        })
6038}
6039
6040/// The `name` a builtin function reports. The table answers for an intrinsic;
6041/// anything else falls back to the last segment of the key, which is what the
6042/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6043/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6044/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6045pub fn builtin_name(key: &str) -> &str {
6046    if let Some((name, _)) = builtin_meta(key) {
6047        return name;
6048    }
6049    match key.strip_prefix("@proto:") {
6050        Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6051        // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6052        // with a `get` prefix), which is what `util.inspect` prints for it and
6053        // what a library reads to identify one.
6054        None => key.rsplit('.').next().unwrap_or(key),
6055    }
6056}
6057
6058/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6059/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6060/// `"get size"` it has to build.
6061pub fn proto_getter_name(key: &str) -> Option<String> {
6062    let (verb, rest) = match key.strip_prefix("@protoget:") {
6063        Some(rest) => ("get", rest),
6064        None => ("set", key.strip_prefix("@protoset:")?),
6065    };
6066    let (_, member) = rest.split_once(':')?;
6067    Some(format!("{verb} {member}"))
6068}
6069
6070pub fn is_known_builtin(name: &str) -> bool {
6071    // Binary search over a sorted INDEX of the two tables rather than a scan of
6072    // both. This runs on every call whose callee is a builtin — `call_method`
6073    // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6074    // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6075    // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6076    // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6077    // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6078    // now. The index is built once per process and derived FROM those tables, so
6079    // it cannot drift from them.
6080    //
6081    // That is an operation count, not a measured time, and NO wall-clock win is
6082    // claimed. Re-measured in isolation (this hunk alone applied to the previous
6083    // commit, interleaved against it, minimums over ten rounds each): the A/B
6084    // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6085    // the A/A control — the SAME binary under both labels — came out 1.084,
6086    // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6087    // this machine the change is not distinguishable from noise. It is kept for
6088    // the comparison count and because it cannot drift from the tables it is
6089    // derived from, not because anything got faster.
6090    static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6091    let sorted = SORTED.get_or_init(|| {
6092        let mut v: Vec<&'static str> = GLOBAL_FUNCS
6093            .iter()
6094            .chain(NS_METHODS.iter())
6095            .copied()
6096            .collect();
6097        v.sort_unstable();
6098        v
6099    });
6100    sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6101}
6102
6103// ── dynamic functions (runtime source → callable) ────────────────────────────
6104
6105/// Build a callable from a complete function-expression source text — the ONE
6106/// dynamic-function generator on this frontend.
6107///
6108/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6109/// wrapping parentheses needed to parse it as an expression: those are added
6110/// here, and `src` itself is retained so `Function.prototype.toString` reports
6111/// what V8 reports. The two callers synthesize different text and both shapes
6112/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6113///
6114/// The body runs in the MODULE scope, never the constructing function's scope
6115/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6116/// *global* environment). That also makes a `var` inside the body a function
6117/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6118/// returns 6 and leaves `globalThis.zz` `undefined`.
6119pub fn dynamic_function(src: &str) -> Result<Value, String> {
6120    let f = crate::eval_in_global_scope(&format!("({src})"))?;
6121    with_host(|h| {
6122        let s = h.new_str(src.to_string());
6123        h.set_fn_prop(&f, "@@source", s);
6124    });
6125    Ok(f)
6126}
6127
6128/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6129///
6130/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6131/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6132/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6133/// arguments at all, both the parameter list and the body are empty.
6134///
6135/// Measured on node v26.7.0:
6136///
6137/// ```text
6138/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6139/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
6140/// new Function('a,b','c','return [a,b,c]').length === 3
6141/// new Function('a','b','return a+b').name       === 'anonymous'
6142/// ```
6143pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6144    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6145    let (params, body) = match parts.split_last() {
6146        Some((body, params)) => (params.join(","), body.clone()),
6147        None => (String::new(), String::new()),
6148    };
6149    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6150}
6151
6152/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6153/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6154/// other route to the same function value is an INDIRECT eval and evaluates in
6155/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6156/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6157/// do not.
6158///
6159/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6160pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6161    let v = arg.cloned().unwrap_or(Value::Undef);
6162    let is_string =
6163        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6164    if !is_string {
6165        return Ok(v);
6166    }
6167    let src = with_host(|h| h.str_of(&v));
6168    // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6169    // decides both the early errors the COMPILE raises and the variable
6170    // environment below. An INDIRECT one is global-scope sloppy code.
6171    let caller_strict = direct && with_host(|h| h.current_strict());
6172    let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6173    if !direct {
6174        return host::run_chunk_in_global_scope(chunk);
6175    }
6176    // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6177    // so its `var`s and function declarations die with it. Only a SLOPPY one
6178    // shares the caller's, which is the form that can inject a binding — and
6179    // sharing it unconditionally meant `eval('var x=1')` inside strict code
6180    // left `x` behind.
6181    let strict = caller_strict
6182        || src.trim_start().starts_with("'use strict'")
6183        || src.trim_start().starts_with("\"use strict\"");
6184    if !strict {
6185        // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6186        // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6187        // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6188        // or `class` declared inside therefore dies with the eval; every one of
6189        // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6190        // behind and `let a=1; eval('let a=2')` overwrote it.
6191        //
6192        // `push_scope` is exactly that split: `var` and a hoisted function
6193        // declaration bind to `base_env`, which this does not touch.
6194        with_host(|h| h.push_scope());
6195        let out = host::run_chunk_on(chunk);
6196        with_host(|h| h.pop_scope());
6197        return out;
6198    }
6199    let prev = with_host(|h| h.push_var_scope());
6200    let out = host::run_chunk_on(chunk);
6201    with_host(|h| h.pop_var_scope(prev));
6202    out
6203}
6204
6205/// Call a resolved builtin function (global or `namespace.method`).
6206pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6207    // `require(spec)`: the ENTRY script's top-level require — core module first,
6208    // else the CommonJS loader resolving from the entry file's directory.
6209    if name == "require" {
6210        let spec = with_host(|h| h.str_of(&arg0(&args)));
6211        return crate::module::require(&spec, &crate::module::entry_dir());
6212    }
6213    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6214    // into the loader, resolving `spec` against the module's own directory.
6215    if name == "__cjs_require" {
6216        let spec = with_host(|h| h.str_of(&arg0(&args)));
6217        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6218        return crate::module::require(&spec, std::path::Path::new(&from));
6219    }
6220    if name == "process.memoryUsage.rss" {
6221        return Ok(crate::stdlib::process::memory_usage_rss());
6222    }
6223    if name == "require.resolve.paths" {
6224        let spec = with_host(|h| h.str_of(&arg0(&args)));
6225        // A core module is not looked up on disk at all.
6226        if crate::stdlib::is_core(&spec) {
6227            return Ok(with_host(|h| h.null()));
6228        }
6229        let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6230        return Ok(with_host(|h| {
6231            let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6232            h.new_array(items)
6233        }));
6234    }
6235    // A `require.extensions` entry. This runtime's loader does not dispatch
6236    // through the map, so calling one is the loader's own behaviour for that
6237    // extension rather than a hook point.
6238    if let Some(ext) = name.strip_prefix("@@extension:") {
6239        let _ = ext;
6240        return Ok(Value::Undef);
6241    }
6242    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6243    if name == "require.resolve" {
6244        let spec = with_host(|h| h.str_of(&arg0(&args)));
6245        if crate::stdlib::is_core(&spec) {
6246            return Ok(with_host(|h| h.new_str(spec)));
6247        }
6248        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6249            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6250            None => Err(crate::host::plain_coded_error(
6251                "Error",
6252                "MODULE_NOT_FOUND",
6253                &format!("Cannot find module '{spec}'"),
6254            )),
6255        };
6256    }
6257    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6258    // path (core modules resolve to the bare specifier, as in Node).
6259    if name == "__cjs_resolve" {
6260        let spec = with_host(|h| h.str_of(&arg0(&args)));
6261        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6262        if crate::stdlib::is_core(&spec) {
6263            return Ok(with_host(|h| h.new_str(spec)));
6264        }
6265        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6266            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6267            None => Err(crate::host::plain_coded_error(
6268                "Error",
6269                "MODULE_NOT_FOUND",
6270                &format!("Cannot find module '{spec}'"),
6271            )),
6272        };
6273    }
6274    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6275    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6276    // stack-introspection pattern used by `depd`), it is called with a synthetic
6277    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6278    if name == "Error.captureStackTrace" {
6279        let target = arg0(&args);
6280        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6281        let stack = match prep {
6282            Some(f)
6283                if matches!(
6284                    with_host(|h| h.get(&f).cloned()),
6285                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6286                ) =>
6287            {
6288                let sites = crate::module::callsite_stack(10)?;
6289                host::invoke(&f, vec![target.clone(), sites], None)?
6290            }
6291            _ => with_host(|h| h.new_str("")),
6292        };
6293        let _ = set_property(&target, "stack", stack);
6294        return Ok(Value::Undef);
6295    }
6296    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6297    if let Some(r) = crate::stdlib::call(name, &args) {
6298        return r;
6299    }
6300    match name {
6301        // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6302        // followed by one `    at <site>` line per call site. Reachable because
6303        // the read hands the hook out, and a library may call it directly to
6304        // render a stack it captured.
6305        DEFAULT_PREPARE => {
6306            let err = arg0(&args);
6307            let header = with_host(|h| {
6308                let name = host::lookup_chain(h, &err, "name")
6309                    .map(|v| h.str_of(&v))
6310                    .unwrap_or_else(|| "Error".to_string());
6311                let msg = host::lookup_chain(h, &err, "message")
6312                    .map(|v| h.str_of(&v))
6313                    .unwrap_or_default();
6314                if msg.is_empty() {
6315                    name
6316                } else {
6317                    format!("{name}: {msg}")
6318                }
6319            });
6320            let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6321            let lines = with_host(|h| match h.get(&sites) {
6322                Some(JsObj::Array(items)) => items.clone(),
6323                _ => Vec::new(),
6324            });
6325            let mut out = header;
6326            for s in lines {
6327                let rendered = host::to_string_value(&s)
6328                    .map(|v| with_host(|h| h.str_of(&v)))
6329                    .unwrap_or_default();
6330                out.push_str("\n    at ");
6331                out.push_str(&rendered);
6332            }
6333            Ok(with_host(|h| h.new_str(out)))
6334        }
6335        "console.log" | "console.info" | "console.debug" => {
6336            print_line(&args, false)?;
6337            Ok(Value::Undef)
6338        }
6339        "console.error" | "console.warn" => {
6340            print_line(&args, true)?;
6341            Ok(Value::Undef)
6342        }
6343        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6344        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6345        // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6346        "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6347        "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6348        "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6349        "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6350        "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6351        "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6352        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6353        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6354        // Reaching `eval` through this table means the eval FUNCTION VALUE was
6355        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6356        // Those are INDIRECT evals and run in the global scope. A literal
6357        // `eval(src)` is intercepted earlier, in `host::call_named`.
6358        "eval" => eval_source(args.first(), false),
6359        // `new Function(...)` and `Function(...)` are the same operation
6360        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6361        // [[Construct]]), so both route to the one generator.
6362        "Function" => function_ctor(&args),
6363        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6364        // (DEP0005). Node still supports it and still routes it to the same place
6365        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6366        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6367        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6368        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6369        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6370        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6371        // where Node warns.
6372        "Buffer" => {
6373            crate::stdlib::process::emit_deprecation_warning(
6374                "DEP0005",
6375                "Buffer() is deprecated due to security and usability issues. \
6376                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6377                 Buffer.from() methods instead.",
6378            );
6379            crate::stdlib::construct("Buffer", &args)
6380                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6381        }
6382        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6383        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6384        "Number.isNaN" => Ok(Value::Bool(
6385            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6386        )),
6387        "Number.isFinite" => Ok(Value::Bool(
6388            matches!(arg0(&args), Value::Float(f) if f.is_finite())
6389                || matches!(arg0(&args), Value::Int(_)),
6390        )),
6391        "String" => {
6392            if args.is_empty() {
6393                Ok(with_host(|h| h.new_str("")))
6394            } else {
6395                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6396                // is allowed); everything else via ToString method dispatch.
6397                host::string_ctor_value(&args[0])
6398            }
6399        }
6400        // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6401        // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6402        // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6403        // one — which is why this cannot just call `to_number_value`.
6404        "Number" => Ok(Value::Float(if args.is_empty() {
6405            0.0
6406        } else {
6407            let prim = host::to_primitive(&args[0], "number")?;
6408            match with_host(|h| h.as_bigint(&prim)) {
6409                Some(b) => host::bigint_to_f64(&b),
6410                None => host::to_number_value(&prim)?,
6411            }
6412        })),
6413        "BigInt" => bigint_ctor(&arg0(&args)),
6414        "RegExp" => regexp_ctor(&args),
6415        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6416        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6417        // Each argument is truncated to a uint16 and taken as one code UNIT, so
6418        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6419        // a surrogate PAIR of arguments composes into one character.
6420        "String.fromCharCode" => Ok(with_host(|h| {
6421            let units: Vec<u16> = args
6422                .iter()
6423                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6424                .collect();
6425            let s = crate::utf16::to_string_lossy(&units);
6426            h.new_str(s)
6427        })),
6428        // `fromCodePoint` takes whole code POINTS and rejects anything that is
6429        // not one — including a lone surrogate, which `fromCharCode` accepts.
6430        "String.fromCodePoint" => {
6431            let mut s = String::new();
6432            for a in &args {
6433                let n = with_host(|h| h.to_number(a));
6434                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6435                {
6436                    char::from_u32(n as u32)
6437                } else {
6438                    None
6439                };
6440                match cp {
6441                    Some(c) => s.push(c),
6442                    None => {
6443                        return Err(format!(
6444                            "RangeError: Invalid code point {}",
6445                            with_host(|h| h.str_of(a))
6446                        ))
6447                    }
6448                }
6449            }
6450            Ok(new_s(s))
6451        }
6452        "String.raw" => string_raw(&args),
6453        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6454        "Array" => construct_builtin("Array", args),
6455        "Array.of" => construct_array_like(host::current_static_this(), args),
6456        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6457        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6458        "Array.isArray" => {
6459            let v = arg0(&args);
6460            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6461            Ok(Value::Bool(
6462                matches!(
6463                    with_host(|h| h.get(&subject).cloned()),
6464                    Some(JsObj::Array(_))
6465                ) && !is_arguments(&subject),
6466            ))
6467        }
6468        "Array.from" => array_from(args),
6469        "Array.fromAsync" => array_from_async(args),
6470        "Object" => Ok(object_call(args)),
6471        "Object.keys" => object_keys(args, 0),
6472        "Object.values" => object_keys(args, 1),
6473        "Object.entries" => object_keys(args, 2),
6474        "Object.assign" => object_assign(args),
6475        "Object.freeze" => {
6476            let v = arg0(&args);
6477            reject_sealing_a_view(&v, "freeze")?;
6478            if seal_proxy(&v, true)? {
6479                return Ok(v);
6480            }
6481            with_host(|h| h.seal_object(&v, true));
6482            Ok(v)
6483        }
6484        "Object.seal" => {
6485            let v = arg0(&args);
6486            reject_sealing_a_view(&v, "seal")?;
6487            if seal_proxy(&v, false)? {
6488                return Ok(v);
6489            }
6490            with_host(|h| h.seal_object(&v, false));
6491            Ok(v)
6492        }
6493        "Object.preventExtensions" => {
6494            let v = arg0(&args);
6495            if crate::proxy::prevent_extensions(&v)? {
6496                return Ok(v);
6497            }
6498            with_host(|h| h.prevent_extensions(&v));
6499            Ok(v)
6500        }
6501        // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6502        // without coercion: it is not extensible, and vacuously frozen and
6503        // sealed. Reporting it extensible and unfrozen was the opposite of
6504        // every one of the three.
6505        "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6506        "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6507        "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6508        "Object.isFrozen" => integrity_level(&arg0(&args), true),
6509        "Object.isSealed" => integrity_level(&arg0(&args), false),
6510        "Object.isExtensible" => {
6511            let v = arg0(&args);
6512            match crate::proxy::is_extensible(&v)? {
6513                Some(b) => Ok(Value::Bool(b)),
6514                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6515            }
6516        }
6517        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6518        // distinct from -0.
6519        "Object.is" => {
6520            let a = arg0(&args);
6521            let b = args.get(1).cloned().unwrap_or(Value::Undef);
6522            let num = |v: &Value| match v {
6523                Value::Int(n) => Some(*n as f64),
6524                Value::Float(f) => Some(*f),
6525                _ => None,
6526            };
6527            let r = match (num(&a), num(&b)) {
6528                (Some(x), Some(y)) => {
6529                    if x.is_nan() && y.is_nan() {
6530                        true
6531                    } else if x == 0.0 && y == 0.0 {
6532                        x.is_sign_negative() == y.is_sign_negative()
6533                    } else {
6534                        x == y
6535                    }
6536                }
6537                _ => with_host(|h| h.strict_eq(&a, &b)),
6538            };
6539            Ok(Value::Bool(r))
6540        }
6541        "Object.fromEntries" => object_from_entries(args),
6542        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6543        // so the proxy form cannot share `prototype_of`'s infallible signature.
6544        // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6545        // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6546        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6547            if name == "Reflect.getPrototypeOf" {
6548                reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6549            }
6550            let v = arg0(&args);
6551            match crate::proxy::get_prototype_of(&v)? {
6552                Some(p) => Ok(p),
6553                None => Ok(prototype_of(&v)),
6554            }
6555        }
6556        "Object.setPrototypeOf" => {
6557            let obj = arg0(&args);
6558            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6559            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6560                reject_bad_prototype(&proto)?;
6561                crate::proxy::set_prototype_of(&obj, &proto)?;
6562                return Ok(obj);
6563            }
6564            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6565            // prototype type check, then — only for an actual object target —
6566            // the extensibility check. A PRIMITIVE target is returned untouched
6567            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6568            // extensibility test cannot come first.
6569            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6570                return Err(host::type_error(
6571                    "Object.setPrototypeOf called on null or undefined",
6572                ));
6573            }
6574            reject_bad_prototype(&proto)?;
6575            if with_host(|h| is_object_like(h, &obj)) {
6576                // Setting the SAME prototype is a no-op and stays legal even on a
6577                // frozen object: node v26.7.0 accepts
6578                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6579                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6580                // link still has `Object.prototype`, and comparing against the
6581                // absent link would call that a change.
6582                if would_cycle(&obj, &proto) {
6583                    return Err(host::type_error("Cyclic __proto__ value"));
6584                }
6585                if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6586                    // The receiver is named by its brand, as every other
6587                    // refusal names it — a NULL-PROTOTYPE object is
6588                    // `[object Object]`, not `#<Object>`, because it has no
6589                    // constructor to name.
6590                    return Err(host::type_error(&format!(
6591                        "{} is not extensible",
6592                        no_side_effects_string(&obj)
6593                    )));
6594                }
6595                with_host(|h| h.set_proto(&obj, proto));
6596            }
6597            Ok(obj)
6598        }
6599        "Object.create" => object_create(args),
6600        "Object.getOwnPropertyNames" => object_keys(args, 3),
6601        "Object.getOwnPropertySymbols" => {
6602            let v = arg0(&args);
6603            require_object_coercible(&v)?;
6604            let syms = proxy_or_own_symbol_keys(&v)?;
6605            Ok(with_host(|h| h.new_array(syms)))
6606        }
6607        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6608        "Object.hasOwn" => {
6609            let obj = arg0(&args);
6610            let key = args.get(1).cloned().unwrap_or(Value::Undef);
6611            object_builtin_method(&obj, "hasOwnProperty", vec![key])
6612        }
6613        "Object.defineProperty" => object_define_property(args),
6614        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6615        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6616        "Object.defineProperties" => object_define_properties(args),
6617        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6618        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6619        "Object.groupBy" => object_group_by(args),
6620        "Symbol" => Ok(with_host(|h| {
6621            let desc = args
6622                .first()
6623                .filter(|a| !matches!(a, Value::Undef))
6624                .map(|a| h.str_of(a));
6625            h.new_symbol(desc)
6626        })),
6627        "Symbol.for" => Ok(with_host(|h| {
6628            let key = h.str_of(&arg0(&args));
6629            h.symbol_for(&key)
6630        })),
6631        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6632        // description read: it answers only for symbols `Symbol.for` created.
6633        // Returning the description made every symbol look registered —
6634        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6635        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6636        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6637        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6638        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6639        "Proxy.revocable" => crate::proxy::revocable(&args),
6640        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6641        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6642        // own properties, so there is no second half to append).
6643        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6644        // non-enumerable included, strings first and then the SYMBOLS.
6645        "Reflect.ownKeys" => {
6646            let v = arg0(&args);
6647            reflect_require_object(&v, "ownKeys")?;
6648            let names = object_keys(args, 3)?;
6649            let syms = proxy_or_own_symbol_keys(&v)?;
6650            if syms.is_empty() {
6651                return Ok(names);
6652            }
6653            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6654            all.extend(syms);
6655            Ok(with_host(|h| h.new_array(all)))
6656        }
6657        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6658        // `Reflect.defineProperty` REPORTS success as a boolean where
6659        // `Object.defineProperty` throws (28.1.3). It was propagating the
6660        // throw, so the whole point of the reflective form was lost.
6661        "Reflect.defineProperty" => {
6662            reflect_require_object(&arg0(&args), "defineProperty")?;
6663            Ok(Value::Bool(object_define_property(args).is_ok()))
6664        }
6665        "Reflect.deleteProperty" => {
6666            let obj = arg0(&args);
6667            reflect_require_object(&obj, "deleteProperty")?;
6668            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6669            Ok(Value::Bool(delete_property(&obj, &k)?))
6670        }
6671        "Reflect.setPrototypeOf" => {
6672            let obj = arg0(&args);
6673            let p = args.get(1).cloned().unwrap_or(Value::Undef);
6674            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6675                crate::proxy::set_prototype_of(&obj, &p)?;
6676                return Ok(Value::Bool(true));
6677            }
6678            // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6679            // unless the new one is what it already has, which is a no-op. It
6680            // reported success and rewrote the link.
6681            // `Reflect` reports a refusal rather than throwing, for a cycle as
6682            // for a non-extensible receiver.
6683            if would_cycle(&obj, &p) {
6684                return Ok(Value::Bool(false));
6685            }
6686            if !with_host(|h| h.is_extensible(&obj)) {
6687                return Ok(Value::Bool(same_prototype(&obj, &p)));
6688            }
6689            with_host(|h| h.set_proto(&obj, p));
6690            Ok(Value::Bool(true))
6691        }
6692        "Reflect.isExtensible" => {
6693            let v = arg0(&args);
6694            match crate::proxy::is_extensible(&v)? {
6695                Some(b) => Ok(Value::Bool(b)),
6696                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6697            }
6698        }
6699        "Reflect.preventExtensions" => {
6700            let v = arg0(&args);
6701            if crate::proxy::prevent_extensions(&v)? {
6702                return Ok(Value::Bool(true));
6703            }
6704            with_host(|h| h.prevent_extensions(&v));
6705            Ok(Value::Bool(true))
6706        }
6707        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6708        "Reflect.apply" => {
6709            let f = arg0(&args);
6710            let this = args.get(1).cloned();
6711            let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6712            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6713        }
6714        // `Reflect.construct(target, args, newTarget)` — the optional third
6715        // argument decides which constructor's `prototype` the instance gets
6716        // (28.1.2). It was ignored, so the result always inherited from
6717        // `target` and `instanceof newTarget` was false.
6718        "Reflect.construct" => {
6719            let f = arg0(&args);
6720            let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6721            let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6722            host::construct_nt(&f, list, new_target)
6723        }
6724        "Reflect.has" => {
6725            let obj = arg0(&args);
6726            reflect_require_object(&obj, "has")?;
6727            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6728            Ok(Value::Bool(has_property(&obj, &k)?))
6729        }
6730        // `Reflect.get(target, key, receiver)` — the optional third argument is
6731        // what a getter sees as `this` (28.1.6). Defaults to the target.
6732        "Reflect.get" => {
6733            let obj = arg0(&args);
6734            reflect_require_object(&obj, "get")?;
6735            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6736            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6737            get_property_recv(&obj, &k, &receiver)
6738        }
6739        // `Reflect.set(target, key, value, receiver)` — the optional fourth
6740        // argument is what a setter sees as `this`, and where a DATA property
6741        // lands (28.1.13). It was ignored: the setter ran against the target
6742        // and the property was written there.
6743        "Reflect.set" => {
6744            let obj = arg0(&args);
6745            reflect_require_object(&obj, "set")?;
6746            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6747            let v = args.get(2).cloned().unwrap_or(Value::Undef);
6748            let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6749            Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6750        }
6751        "JSON.stringify" => json_stringify(args),
6752        "JSON.parse" => json_parse(args),
6753        "JSON.rawJSON" => json_raw(args),
6754        "JSON.isRawJSON" => json_is_raw(args),
6755        "structuredClone" => structured_clone(args),
6756        // The deferred drain a `Readable.from` schedules; the suffix is the
6757        // stream's heap index.
6758        _ if name.starts_with("@@transformCb:") => {
6759            let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6760            crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6761            Ok(Value::Undef)
6762        }
6763        _ if name.starts_with("@@streamFlush:") => {
6764            let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6765            crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6766            Ok(Value::Undef)
6767        }
6768        // Same implementation the `buffer` module exposes; only the binding was
6769        // missing.
6770        "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6771            .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6772        "fetch" => crate::stdlib::fetch::fetch(&args),
6773        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6774        // suffix is the signal's heap index.
6775        _ if name.starts_with("@@aborttimeout:") => {
6776            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6777            crate::stdlib::fetch::fire_timeout_abort(idx)
6778        }
6779        // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6780        // implementation. Nothing here waits on backpressure, so it only has to
6781        // BE callable — an implementation that ends with `cb()`, which the
6782        // stream contract requires, would otherwise throw.
6783        "@@streamWriteCallback" => Ok(Value::Undef),
6784        "queueMicrotask" | "process.nextTick" => {
6785            let cb = arg0(&args);
6786            require_callback(&cb)?;
6787            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6788            enqueue_microtask(name == "process.nextTick", cb, rest);
6789            Ok(Value::Undef)
6790        }
6791        "setTimeout" | "setInterval" | "setImmediate" => {
6792            require_callback(&arg0(&args))?;
6793            Ok(schedule_timer(name, args))
6794        }
6795        "clearTimeout" | "clearInterval" | "clearImmediate" => {
6796            clear_timer(&arg0(&args));
6797            Ok(Value::Undef)
6798        }
6799        "Promise.resolve" => promise_resolve(arg0(&args)),
6800        "Promise.reject" => promise_reject(arg0(&args)),
6801        "Promise.all" => promise_all(args, AllMode::All),
6802        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6803        "Promise.race" => promise_race(args, false),
6804        "Promise.any" => promise_race(args, true),
6805        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6806        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6807        "Promise.withResolvers" => promise_with_resolvers(),
6808        "Promise.try" => promise_try(args),
6809        "RegExp.escape" => regexp_escape(args),
6810        "Error.isError" => error_is_error(args),
6811        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6812        // `cb(item, i)` result (SameValueZero), each value an array of members.
6813        "Map.groupBy" => map_group_by(args),
6814        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6815        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6816        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6817        // The executor a species-constructed promise is built with: it does
6818        // nothing, because the caller settles the result through its id.
6819        "@@pnoop" => Ok(Value::Undef),
6820        _ if name.starts_with("@@presolve:") => {
6821            let id: u32 = name[11..].parse().unwrap_or(0);
6822            host::resolve_promise_val(id, arg0(&args));
6823            Ok(Value::Undef)
6824        }
6825        _ if name.starts_with("@@preject:") => {
6826            let id: u32 = name[10..].parse().unwrap_or(0);
6827            host::reject_promise_val(id, arg0(&args));
6828            Ok(Value::Undef)
6829        }
6830        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6831        // index so calling it twice is the spec's no-op rather than a re-tear.
6832        _ if name.starts_with("@@prevoke:") => {
6833            let i: u32 = name[10..].parse().unwrap_or(0);
6834            Ok(crate::proxy::revoke(i))
6835        }
6836        _ if name.starts_with("@@finpass:") => {
6837            // finally(cb) on fulfill: run cb, await whatever it returned, then
6838            // pass the original value through.
6839            let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6840            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6841            Ok(finally_chain(result, arg0(&args), false))
6842        }
6843        _ if name.starts_with("@@finthrow:") => {
6844            // finally(cb) on reject: same, then re-throw the original reason.
6845            let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6846            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6847            Ok(finally_chain(result, arg0(&args), true))
6848        }
6849        // The two thunks `finally_chain` hangs off that awaited promise. Each
6850        // carries the value it must reinstate in a one-slot cell, since a
6851        // builtin is identified only by its name and cannot close over one.
6852        _ if name.starts_with("@@finret:") => {
6853            let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6854            get_property(&Value::Obj(i), "0")
6855        }
6856        _ if name.starts_with("@@finrethrow:") => {
6857            let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6858            let reason = get_property(&Value::Obj(i), "0")?;
6859            with_host(|h| h.exc = Some(reason.clone()));
6860            Err(with_host(|h| error_string(h, &reason)))
6861        }
6862        _ => Err(host::type_error(&format!("{name} is not a function"))),
6863    }
6864}
6865
6866/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6867/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6868/// (matching Node's messages).
6869/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6870/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6871/// BigInt`. The old text said "value" literally, for every input.
6872fn bigint_convert_error(v: &Value) -> String {
6873    let shown = with_host(|h| h.str_of(v));
6874    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6875}
6876
6877/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6878/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6879/// BigInt arithmetic's operand check.
6880///
6881/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6882/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6883/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6884/// the type table — booleans convert (`true` → `1n`), strings parse with a
6885/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6886///
6887/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6888///
6889/// ```text
6890/// a[0] = true            → 1n
6891/// a[0] = '12'            → 12n
6892/// a[0] = []              → 0n        (ToPrimitive → "" → 0n)
6893/// a[0] = ['3']           → 3n
6894/// a[0] = 1               → TypeError: Cannot convert 1 to a BigInt
6895/// a[0] = new Number(3)   → TypeError: Cannot convert 3 to a BigInt
6896/// a[0] = 'a'             → SyntaxError: Cannot convert a to a BigInt
6897/// a[0] = {}              → SyntaxError: Cannot convert [object Object] to a BigInt
6898/// ```
6899pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6900    let prim = host::to_primitive(v, "number")?;
6901    if let Some(b) = with_host(|h| match h.get(&prim) {
6902        Some(JsObj::BigInt(b)) => Some(b.clone()),
6903        _ => None,
6904    }) {
6905        return Ok(b);
6906    }
6907    match &prim {
6908        Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6909        Value::Str(s) => host::parse_bigint_str(s)
6910            .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6911        _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6912            let s = with_host(|h| h.str_of(&prim));
6913            host::parse_bigint_str(&s)
6914                .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6915        }
6916        _ => Err(bigint_convert_error(&prim)),
6917    }
6918}
6919
6920fn bigint_ctor(v: &Value) -> Result<Value, String> {
6921    use num_bigint::BigInt;
6922    let big = match v {
6923        Value::Bool(b) => BigInt::from(*b as i64),
6924        Value::Int(n) => BigInt::from(*n),
6925        Value::Float(f) => {
6926            if !f.is_finite() || f.fract() != 0.0 {
6927                let disp = with_host(|h| h.str_of(v));
6928                return Err(format!(
6929                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6930                ));
6931            }
6932            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6933            // .toString` switches to exponential notation at 1e21, and
6934            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6935            // threw `Cannot convert value to a BigInt` where node returns
6936            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6937            // value, which is also what node reports for a magnitude past the
6938            // exactly-representable range (`BigInt(1e30)` is
6939            // `1000000000000000019884624838656n` in both).
6940            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6941                Some(b) => b,
6942                None => return Err(bigint_convert_error(v)),
6943            }
6944        }
6945        Value::Str(s) => match host::parse_bigint_str(s) {
6946            Some(b) => b,
6947            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6948        },
6949        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6950            Some(JsObj::BigInt(b)) => b,
6951            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6952                Some(b) => b,
6953                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6954            },
6955            _ => return Err(bigint_convert_error(v)),
6956        },
6957        _ => return Err(bigint_convert_error(v)),
6958    };
6959    Ok(with_host(|h| h.new_bigint(big)))
6960}
6961
6962/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6963/// its source (and flags, unless new ones are given).
6964fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6965    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6966        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6967        _ => {
6968            let a0 = arg0(args);
6969            // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6970            // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6971            // pattern instead of throwing.
6972            let src = if matches!(a0, Value::Undef) {
6973                String::new()
6974            } else {
6975                arg_to_string(args, 0)?
6976            };
6977            (src, None)
6978        }
6979    };
6980    let flags = match args.get(1) {
6981        Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6982        _ => existing_flags.unwrap_or_default(),
6983    };
6984    // An empty source compiles as the JS canonical `(?:)`.
6985    let src = if source.is_empty() {
6986        "(?:)".to_string()
6987    } else {
6988        source
6989    };
6990    crate::regexp::build_regexp(&src, &flags)
6991}
6992
6993/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
6994/// two's-complement (signed) or unsigned integer.
6995fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
6996    use num_bigint::BigInt;
6997    use num_traits::Signed;
6998    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
6999    if bits < 0 {
7000        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7001    }
7002    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7003        Some(b) => b,
7004        None => return Err(host::type_error("Cannot convert to a BigInt")),
7005    };
7006    let bits = bits as u32;
7007    if bits == 0 {
7008        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7009    }
7010    let modulus = BigInt::from(1) << bits; // 2^bits
7011                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7012    let mut r = &x % &modulus;
7013    if r.is_negative() {
7014        r += &modulus;
7015    }
7016    if !unsigned {
7017        let half = BigInt::from(1) << (bits - 1);
7018        if r >= half {
7019            r -= &modulus;
7020        }
7021    }
7022    Ok(with_host(|h| h.new_bigint(r)))
7023}
7024
7025/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7026/// interleaved with the substitutions.
7027fn string_raw(args: &[Value]) -> Result<Value, String> {
7028    let call_site = arg0(args);
7029    let raw = get_property(&call_site, "raw")?;
7030    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7031    let mut out = String::new();
7032    for (i, r) in raws.iter().enumerate() {
7033        out.push_str(&with_host(|h| h.str_of(r)));
7034        if i + 1 < raws.len() {
7035            if let Some(sub) = args.get(i + 1) {
7036                out.push_str(&with_host(|h| h.str_of(sub)));
7037            }
7038        }
7039    }
7040    Ok(with_host(|h| h.new_str(out)))
7041}
7042
7043/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7044/// fresh object; objects pass through.
7045/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7046/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7047/// ordinary object that also has internal slots, so it can carry own properties
7048/// like anything else — but its heap variant holds only those slots, so a write
7049/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7050pub fn uses_side_table(v: &Value) -> bool {
7051    matches!(
7052        with_host(|h| h.kind_of(v)),
7053        Some(
7054            ObjKind::Map
7055                | ObjKind::Set
7056                | ObjKind::Promise
7057                | ObjKind::RegExp
7058                | ObjKind::Generator
7059                | ObjKind::Symbol
7060                | ObjKind::BigInt
7061                | ObjKind::Iter
7062        )
7063    )
7064}
7065
7066fn object_call(args: Vec<Value>) -> Value {
7067    let a = arg0(&args);
7068    // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7069    // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7070    if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7071        return with_host(|h| h.new_object(IndexMap::new()));
7072    }
7073    to_object(&a)
7074}
7075
7076/// The name of the wrapper a primitive boxes into, or `None` when the value is
7077/// already an object.
7078fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7079    match v {
7080        Value::Int(_) | Value::Float(_) => Some("Number"),
7081        Value::Bool(_) => Some("Boolean"),
7082        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7083            Some(JsObj::Str(_)) => Some("String"),
7084            Some(JsObj::Symbol { .. }) => Some("Symbol"),
7085            Some(JsObj::BigInt(_)) => Some("BigInt"),
7086            _ => None,
7087        },
7088        _ => None,
7089    }
7090}
7091
7092/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7093/// for every other value. The slot is a hidden `@@primitive` own property —
7094/// the same `@@` marker convention the engine already uses for internal state,
7095/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7096pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7097    with_host(|h| match h.get(v) {
7098        Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7099        _ => None,
7100    })
7101}
7102
7103/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7104/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7105///
7106/// A String wrapper also owns its index properties and `length`, which is what
7107/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7108/// non-writable and non-configurable, as the exotic `String` object's are.
7109pub fn to_object(v: &Value) -> Value {
7110    let Some(ctor) = wrapper_ctor_of(v) else {
7111        return v.clone();
7112    };
7113    with_host(|h| h.ensure_wrapper_protos());
7114    let chars: Vec<String> = if ctor == "String" {
7115        with_host(|h| h.str_of(v))
7116            .chars()
7117            .map(|c| c.to_string())
7118            .collect()
7119    } else {
7120        Vec::new()
7121    };
7122    with_host(|h| {
7123        let mut m: IndexMap<String, Value> = IndexMap::new();
7124        for (i, c) in chars.iter().enumerate() {
7125            let s = h.new_str(c.clone());
7126            m.insert(i.to_string(), s);
7127        }
7128        let w = h.new_object(m);
7129        if ctor == "String" {
7130            for i in 0..chars.len() {
7131                h.set_prop_attrs(
7132                    &w,
7133                    &i.to_string(),
7134                    host::PropAttrs {
7135                        writable: false,
7136                        enumerable: true,
7137                        configurable: false,
7138                    },
7139                );
7140            }
7141            let len = Value::Float(chars.len() as f64);
7142            if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7143                p.insert("length".into(), len);
7144            }
7145            h.set_prop_attrs(
7146                &w,
7147                "length",
7148                host::PropAttrs {
7149                    writable: false,
7150                    enumerable: false,
7151                    configurable: false,
7152                },
7153            );
7154        }
7155        if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7156            p.insert("@@primitive".into(), v.clone());
7157        }
7158        if let Some(proto) = h.native_proto(ctor) {
7159            h.set_proto(&w, proto);
7160        }
7161        w
7162    })
7163}
7164
7165/// Construct via `new` for the builtin constructors.
7166pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7167    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7168    if let Some(r) = crate::stdlib::construct(name, &args) {
7169        return r;
7170    }
7171    match name {
7172        "Array" => {
7173            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7174            // A single NUMBER argument is a length and is validated as one
7175            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7176            // `new Array(2**32)` are all `RangeError: Invalid array length` on
7177            // node v26.7.0; only a non-number single argument is an element.
7178            if args.len() == 1 {
7179                if let Value::Float(_) | Value::Int(_) = args[0] {
7180                    let n = host::to_array_length(&args[0])?;
7181                    // Every element of `new Array(n)` is a HOLE, not a stored
7182                    // `undefined`: `Object.keys(Array(3))` is `[]`.
7183                    return Ok(with_host(|h| {
7184                        let a = h.new_array(vec![Value::Undef; n]);
7185                        h.mark_hole_range(&a, 0..n);
7186                        a
7187                    }));
7188                }
7189            }
7190            Ok(with_host(|h| h.new_array(args)))
7191        }
7192        "Object" => Ok(object_call(args)),
7193        // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7194        // form. These were not constructors at all, so every one threw.
7195        "String" => Ok(to_object(&host::to_string_value(
7196            &args
7197                .first()
7198                .cloned()
7199                .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7200        )?)),
7201        "Number" => Ok(to_object(&Value::Float(match args.first() {
7202            Some(a) => host::to_number_value(a)?,
7203            None => 0.0,
7204        }))),
7205        "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7206            h.truthy(&arg0(&args))
7207        })))),
7208        "Map" | "WeakMap" => {
7209            let weak = name == "WeakMap";
7210            let m = with_host(|h| {
7211                h.alloc(JsObj::Map {
7212                    entries: indexmap::IndexMap::new(),
7213                    weak,
7214                })
7215            });
7216            if let Some(init) = args
7217                .first()
7218                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7219            {
7220                // Stepped, not drained: an entry that is not a pair has to
7221                // stop the construction at that element and CLOSE the iterator
7222                // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7223                // infinite source was never reached and the constructor HUNG.
7224                host::iter_for_each(init, |p, _| {
7225                    // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7226                    // is iterable, so without this check `new Map(["ab"])`
7227                    // happily stored `'a' => 'b'` instead of throwing — and over
7228                    // an infinite source it never stopped.
7229                    if !with_host(|h| is_object_like(h, &p)) {
7230                        let shown = with_host(|h| h.str_of(&p));
7231                        return Err(host::type_error(&format!(
7232                            "Iterator value {shown} is not an entry object"
7233                        )));
7234                    }
7235                    // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7236                    // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7237                    // gives `undefined => undefined`, and an array-LIKE entry
7238                    // works. Iterating it instead accepted a string as a pair
7239                    // and rejected the array-like.
7240                    let k = get_property(&p, "0")?;
7241                    let v = get_property(&p, "1")?;
7242                    map_method(&m, "set", vec![k, v])?;
7243                    Ok(())
7244                })?;
7245            }
7246            Ok(m)
7247        }
7248        "Set" | "WeakSet" => {
7249            let weak = name == "WeakSet";
7250            let s = with_host(|h| {
7251                h.alloc(JsObj::Set {
7252                    entries: indexmap::IndexMap::new(),
7253                    weak,
7254                })
7255            });
7256            if let Some(init) = args
7257                .first()
7258                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7259            {
7260                host::iter_for_each(init, |v, _| {
7261                    set_method(&s, "add", vec![v])?;
7262                    Ok(())
7263                })?;
7264            }
7265            Ok(s)
7266        }
7267        "Promise" => new_promise(arg0(&args)),
7268        "Proxy" => crate::proxy::create(&args),
7269        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7270        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7271        // deprecation wrapper this way, so `require('body-parser')` — and with it
7272        // `require('express')` — dies at load without it.
7273        "Function" => function_ctor(&args),
7274        "RegExp" => regexp_ctor(&args),
7275        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7276        "Error" => make_error_checked(name, &args),
7277        // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7278        // legacy numeric `code` follows from it.
7279        "DOMException" => Ok(dom_exception(&args)),
7280        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7281        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7282    }
7283}
7284
7285/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7286/// outside the table — including the default `"Error"` — reports 0.
7287pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7288    ("IndexSizeError", 1.0),
7289    ("DOMStringSizeError", 2.0),
7290    ("HierarchyRequestError", 3.0),
7291    ("WrongDocumentError", 4.0),
7292    ("InvalidCharacterError", 5.0),
7293    ("NoDataAllowedError", 6.0),
7294    ("NoModificationAllowedError", 7.0),
7295    ("NotFoundError", 8.0),
7296    ("NotSupportedError", 9.0),
7297    ("InUseAttributeError", 10.0),
7298    ("InvalidStateError", 11.0),
7299    ("SyntaxError", 12.0),
7300    ("InvalidModificationError", 13.0),
7301    ("NamespaceError", 14.0),
7302    ("InvalidAccessError", 15.0),
7303    ("ValidationError", 16.0),
7304    ("TypeMismatchError", 17.0),
7305    ("SecurityError", 18.0),
7306    ("NetworkError", 19.0),
7307    ("AbortError", 20.0),
7308    ("URLMismatchError", 21.0),
7309    ("QuotaExceededError", 22.0),
7310    ("TimeoutError", 23.0),
7311    ("InvalidNodeTypeError", 24.0),
7312    ("DataCloneError", 25.0),
7313];
7314
7315/// The static name a `DOMException` code is exposed under: the error name minus
7316/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7317/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7318fn legacy_code_name(error_name: &str) -> String {
7319    let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7320    let mut out = String::new();
7321    for (i, c) in stem.chars().enumerate() {
7322        if c.is_ascii_uppercase() && i > 0 {
7323            out.push('_');
7324        }
7325        out.push(c.to_ascii_uppercase());
7326    }
7327    out.push_str("_ERR");
7328    out
7329}
7330
7331/// `new DOMException(message, name)`.
7332///
7333/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7334/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7335/// legacy number that name maps to.
7336pub fn dom_exception(args: &[Value]) -> Value {
7337    let message = match args.first() {
7338        None | Some(Value::Undef) => String::new(),
7339        Some(v) => with_host(|h| h.str_of(v)),
7340    };
7341    let name = match args.get(1) {
7342        None | Some(Value::Undef) => "Error".to_string(),
7343        Some(v) => with_host(|h| h.str_of(v)),
7344    };
7345    with_host(|h| dom_exception_with(h, &name, &message))
7346}
7347
7348/// `dom_exception` for a caller that already holds the host borrow.
7349pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7350    let name = name.to_string();
7351    let message = message.to_string();
7352    let code = DOM_EXCEPTION_CODES
7353        .iter()
7354        .find(|(n, _)| *n == name)
7355        .map(|(_, c)| *c)
7356        .unwrap_or(0.0);
7357    let head = if message.is_empty() {
7358        name.clone()
7359    } else {
7360        format!("{name}: {message}")
7361    };
7362    let e = synth_error(h, &head);
7363    {
7364        let nv = h.new_str(name);
7365        let mv = h.new_str(message);
7366        let sv = h.new_str(head);
7367        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7368            // `name`, `message` and `code` are PROTOTYPE accessors over internal
7369            // slots in node, so `stack` is the instance's only own property.
7370            // Storing them as own keys would show up in
7371            // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7372            p.shift_remove("message");
7373            p.insert("@@domName".into(), nv);
7374            p.insert("@@domMessage".into(), mv);
7375            p.insert("@@domCode".into(), Value::Float(code));
7376            p.insert("stack".into(), sv);
7377        }
7378        h.ensure_error_protos();
7379        if let Some(proto) = host::error_proto_of(h, "DOMException") {
7380            h.set_proto(&e, proto);
7381        }
7382    }
7383    e
7384}
7385
7386/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7387/// rather than as own properties. `None` for anything else.
7388pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7389    let slot = match name {
7390        "name" => "@@domName",
7391        "message" => "@@domMessage",
7392        "code" => "@@domCode",
7393        _ => return None,
7394    };
7395    with_host(|h| match h.get(recv) {
7396        Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7397        _ => None,
7398    })
7399}
7400
7401/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7402/// throw a value with extra own properties on it.
7403pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7404    let m = with_host(|h| h.new_str(msg.to_string()));
7405    make_error_inner(name, &[m])
7406}
7407
7408/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7409/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7410/// rendered `Symbol(desc)` into `.message`.
7411fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7412    if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7413        // AggregateError's message is its SECOND argument.
7414        let idx = usize::from(name == "AggregateError");
7415        if idx == 0 {
7416            host::to_string_value(m)?;
7417        } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7418            host::to_string_value(m2)?;
7419        }
7420    }
7421    Ok(make_error_inner(name, args))
7422}
7423
7424fn make_error_inner(name: &str, args: &[Value]) -> Value {
7425    // `new AggregateError(errors, message)` takes the causes FIRST; every other
7426    // error constructor takes the message first.
7427    let agg = name == "AggregateError";
7428    let (errors, args) = if agg {
7429        (
7430            Some(args.first().cloned().unwrap_or(Value::Undef)),
7431            args.get(1..).unwrap_or(&[]),
7432        )
7433    } else {
7434        (None, args)
7435    };
7436    with_host(|h| {
7437        h.ensure_error_protos();
7438        let mut props: IndexMap<String, Value> = IndexMap::new();
7439        let msg = args
7440            .first()
7441            .filter(|a| !matches!(a, Value::Undef))
7442            .map(|a| h.str_of(a));
7443        if let Some(m) = &msg {
7444            let mv = h.new_str(m.clone());
7445            props.insert("message".into(), mv);
7446        }
7447        // `.stack` is engine-specific; a simple `Name: message` header line
7448        // suffices for parity (the fuzzer never prints raw stacks).
7449        //
7450        // V8 formats that header LAZILY, on the first read, from whatever `name`
7451        // and `message` the error carries at that moment — which is why the
7452        // near-universal
7453        //
7454        //     class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7455        //
7456        // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7457        // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7458        // carries the frames so the read can redo it; see `materialize_stack`.
7459        let frames = h.stack_frames();
7460        let stack = match &msg {
7461            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7462            _ => format!("{name}{frames}"),
7463        };
7464        let sv = h.new_str(stack);
7465        props.insert("stack".into(), sv);
7466        let raw = h.new_str(frames);
7467        props.insert("@@stackRaw".into(), raw);
7468        if let Some(errs) = errors {
7469            // Materialize the iterable into the own `errors` array property.
7470            let items = h.iter_vec(&errs).unwrap_or_default();
7471            let arr = h.new_array(items);
7472            props.insert("errors".into(), arr);
7473        }
7474        // `new Error(msg, { cause })` (ES2022): installed only when the options
7475        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7476        let opts = args.get(1);
7477        if let Some(cause) = opts.and_then(|o| match h.get(o) {
7478            Some(JsObj::Object(p)) => p.get("cause").cloned(),
7479            _ => None,
7480        }) {
7481            props.insert("cause".into(), cause);
7482        }
7483        let e = h.new_object(props);
7484        if let Some(p) = host::error_proto_of(h, name) {
7485            h.set_proto(&e, p);
7486        }
7487        // Every own slot an error constructor installs is non-enumerable in V8,
7488        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7489        // `{}` — properties a *script* later assigns stay enumerable.
7490        for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7491            h.hide_prop(&e, k);
7492        }
7493        e
7494    })
7495}
7496
7497fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7498    // Node's console.log(...args) === util.format(...args): printf-style
7499    // substitution when the first arg is a format string, else inspect-and-join.
7500    // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7501    // the caller instead of printing a line — so nothing is written on failure.
7502    let line: String = crate::stdlib::util::format(args)?;
7503    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7504    Ok(())
7505}
7506
7507fn arg0(args: &[Value]) -> Value {
7508    args.first().cloned().unwrap_or(Value::Undef)
7509}
7510/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7511/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7512/// instead rendered `Symbol(desc)` into its result and reported nothing.
7513fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7514    let v = args.get(i).cloned().unwrap_or(Value::Undef);
7515    let sv = host::to_string_value(&v)?;
7516    Ok(with_host(|h| h.str_of(&sv)))
7517}
7518
7519fn arg_num(args: &[Value], i: usize) -> f64 {
7520    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7521}
7522
7523fn is_integer(v: Value) -> bool {
7524    match v {
7525        Value::Int(_) => true,
7526        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7527        _ => false,
7528    }
7529}
7530fn is_safe_integer(v: Value) -> bool {
7531    match v {
7532        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7533        Value::Int(_) => true,
7534        _ => false,
7535    }
7536}
7537
7538/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7539/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7540/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7541fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7542    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7543    const UNRESERVED: &[u8] =
7544        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7545    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7546    const RESERVED: &[u8] = b";,/?:@&=+$#";
7547    let mut out = String::with_capacity(s.len());
7548    for &b in s.as_bytes() {
7549        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7550            out.push(b as char);
7551        } else {
7552            out.push('%');
7553            out.push(
7554                char::from_digit((b >> 4) as u32, 16)
7555                    .unwrap()
7556                    .to_ascii_uppercase(),
7557            );
7558            out.push(
7559                char::from_digit((b & 0xf) as u32, 16)
7560                    .unwrap()
7561                    .to_ascii_uppercase(),
7562            );
7563        }
7564    }
7565    Ok(with_host(|h| h.new_str(out)))
7566}
7567
7568/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7569/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7570/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7571fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7572    const RESERVED: &[u8] = b";,/?:@&=+$#";
7573    let bytes = s.as_bytes();
7574    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7575    let mut i = 0;
7576    while i < bytes.len() {
7577        if bytes[i] == b'%' {
7578            if i + 2 >= bytes.len() {
7579                return Err("URIError: URI malformed".into());
7580            }
7581            let hi = (bytes[i + 1] as char).to_digit(16);
7582            let lo = (bytes[i + 2] as char).to_digit(16);
7583            match (hi, lo) {
7584                (Some(h), Some(l)) => {
7585                    let byte = (h * 16 + l) as u8;
7586                    // decodeURI keeps reserved-delimiter escapes literal.
7587                    if uri && RESERVED.contains(&byte) {
7588                        out.extend_from_slice(&bytes[i..i + 3]);
7589                    } else {
7590                        out.push(byte);
7591                    }
7592                    i += 3;
7593                }
7594                _ => return Err("URIError: URI malformed".into()),
7595            }
7596        } else {
7597            out.push(bytes[i]);
7598            i += 1;
7599        }
7600    }
7601    match String::from_utf8(out) {
7602        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7603        Err(_) => Err("URIError: URI malformed".into()),
7604    }
7605}
7606
7607/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7608/// present in every engine and still reached by old libraries (jQuery's cookie
7609/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7610/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7611/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7612/// character yields the two escapes of its surrogate pair
7613/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7614///
7615/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7616/// it keeps `@*_+-./` and drops `!~'()`.
7617fn legacy_escape(s: &str) -> Result<Value, String> {
7618    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7619    let mut out = String::with_capacity(s.len());
7620    for u in s.encode_utf16() {
7621        if u < 0x100 {
7622            if KEEP.contains(&(u as u8)) {
7623                out.push(u as u8 as char);
7624            } else {
7625                out.push_str(&format!("%{u:02X}"));
7626            }
7627        } else {
7628            out.push_str(&format!("%u{u:04X}"));
7629        }
7630    }
7631    Ok(with_host(|h| h.new_str(out)))
7632}
7633
7634/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7635/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7636/// `%XX` or `%uXXXX` escape is passed through literally
7637/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7638///
7639/// Decoding is done in code-unit space and re-joined at the end so a
7640/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7641fn legacy_unescape(s: &str) -> Result<Value, String> {
7642    let b = s.as_bytes();
7643    let hex = |i: usize, n: usize| -> Option<u16> {
7644        if i + n > b.len() {
7645            return None;
7646        }
7647        let mut v: u16 = 0;
7648        for &c in &b[i..i + n] {
7649            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7650        }
7651        Some(v)
7652    };
7653    let units: Vec<u16> = s.encode_utf16().collect();
7654    let mut out: Vec<u16> = Vec::with_capacity(units.len());
7655    let mut i = 0;
7656    while i < b.len() {
7657        // Escapes are pure ASCII, so a byte index is a unit index up to here —
7658        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7659        if b[i] == b'%' {
7660            if let Some(u) = hex(i + 1, 2) {
7661                out.push(u);
7662                i += 3;
7663                continue;
7664            }
7665            if b.get(i + 1) == Some(&b'u') {
7666                if let Some(u) = hex(i + 2, 4) {
7667                    out.push(u);
7668                    i += 6;
7669                    continue;
7670                }
7671            }
7672        }
7673        let c = s[i..].chars().next().unwrap_or('%');
7674        let mut buf = [0u16; 2];
7675        out.extend_from_slice(c.encode_utf16(&mut buf));
7676        i += c.len_utf8();
7677    }
7678    Ok(with_host(|h| {
7679        h.new_str(crate::utf16::to_string_lossy(&out))
7680    }))
7681}
7682
7683/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7684/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7685/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7686/// instead of converting it, which both swallowed that throw and ignored any
7687/// `toString` the value defines.
7688fn parse_int(args: &[Value]) -> Result<f64, String> {
7689    // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7690    let sv = host::to_string_value(&arg0(args))?;
7691    // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7692    // infallible read below does no `ToPrimitive`, so an object radix came out
7693    // as NaN and the parse silently fell back to auto-detection.
7694    let radix = match args.get(1) {
7695        Some(r) if !matches!(r, Value::Undef) => {
7696            vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7697        }
7698        _ => args.to_vec(),
7699    };
7700    Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7701}
7702
7703fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7704    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7705    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7706    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7707    let radix_arg = args
7708        .get(1)
7709        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7710    let radix = match radix_arg {
7711        Some(0) | None => None,
7712        Some(r) if (2..=36).contains(&r) => Some(r as u32),
7713        Some(_) => return f64::NAN,
7714    };
7715    let t = crate::utf16::js_trim_start(s);
7716    let (neg, digits) = match t.strip_prefix('-') {
7717        Some(rest) => (true, rest),
7718        None => (false, t.strip_prefix('+').unwrap_or(t)),
7719    };
7720    let (radix, digits) = match radix {
7721        Some(16) => (
7722            16u32,
7723            digits
7724                .strip_prefix("0x")
7725                .or_else(|| digits.strip_prefix("0X"))
7726                .unwrap_or(digits),
7727        ),
7728        Some(r) => (r, digits),
7729        None => {
7730            if let Some(hex) = digits
7731                .strip_prefix("0x")
7732                .or_else(|| digits.strip_prefix("0X"))
7733            {
7734                (16, hex)
7735            } else {
7736                (10, digits)
7737            }
7738        }
7739    };
7740    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7741    if valid.is_empty() {
7742        return f64::NAN;
7743    }
7744    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7745    // digits and the error was mapped to `NaN`, so
7746    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7747    // asks for the mathematical value rounded to a Number, which is what
7748    // repeated multiply-accumulate in `f64` produces.
7749    let n = if radix == 10 {
7750        // Rust's decimal float parser is correctly rounded; digit-by-digit
7751        // multiply-accumulate is not, and drifted a ULP on long inputs
7752        // (`parseInt("999999999999999999999999")` came out
7753        // 1.0000000000000003e+24 rather than 1e+24).
7754        valid.parse::<f64>().unwrap_or(f64::NAN)
7755    } else {
7756        let mut n = 0.0f64;
7757        for c in valid.chars() {
7758            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7759        }
7760        n
7761    };
7762    if neg {
7763        -n
7764    } else {
7765        n
7766    }
7767}
7768
7769/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7770fn parse_float(args: &[Value]) -> Result<f64, String> {
7771    let sv = host::to_string_value(&arg0(args))?;
7772    Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7773}
7774
7775fn parse_float_str(s: &str) -> f64 {
7776    let t = crate::utf16::js_trim_start(s);
7777    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7778    let inf_body = t
7779        .strip_prefix('+')
7780        .or_else(|| t.strip_prefix('-'))
7781        .unwrap_or(t);
7782    if inf_body.starts_with("Infinity") {
7783        return if t.starts_with('-') {
7784            f64::NEG_INFINITY
7785        } else {
7786            f64::INFINITY
7787        };
7788    }
7789    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7790    // not the same as the longest run of characters that could appear in one:
7791    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7792    // only valid once a digit follows `e`. Tracking `end` at every character
7793    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7794    // came back NaN.
7795    let mut end = 0;
7796    let bytes = t.as_bytes();
7797    let mut seen_dot = false;
7798    let mut seen_e = false;
7799    let mut digits_before_dot = false;
7800    for (i, &c) in bytes.iter().enumerate() {
7801        match c {
7802            b'0'..=b'9' => {
7803                if !seen_dot && !seen_e {
7804                    digits_before_dot = true;
7805                }
7806                end = i + 1;
7807            }
7808            // A sign is only meaningful leading, or straight after the exponent
7809            // marker; it never completes a literal on its own.
7810            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7811            // `1.` is a complete literal; a bare `.` is not.
7812            b'.' if !seen_dot && !seen_e => {
7813                seen_dot = true;
7814                if digits_before_dot {
7815                    end = i + 1;
7816                }
7817            }
7818            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7819            _ => break,
7820        }
7821    }
7822    if end == 0 {
7823        return f64::NAN;
7824    }
7825    t[..end].parse::<f64>().unwrap_or(f64::NAN)
7826}
7827
7828/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7829/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7830/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7831/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7832/// rather than 1.
7833pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7834    if exp == 0.0 {
7835        return 1.0;
7836    }
7837    if base.is_nan() || exp.is_nan() {
7838        return f64::NAN;
7839    }
7840    if base.abs() == 1.0 && exp.is_infinite() {
7841        return f64::NAN;
7842    }
7843    base.powf(exp)
7844}
7845
7846fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7847    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7848    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7849    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7850    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7851    // exception: it never reads an argument, so `Math.random(1n)` is fine.
7852    // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7853    // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7854    // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7855    // the host itself and cannot run inside another borrow.
7856    let is_bigint = |a: &Value| {
7857        if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7858            return true;
7859        }
7860        match wrapped_primitive(a) {
7861            Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7862            None => false,
7863        }
7864    };
7865    if fname != "random" && args.iter().any(is_bigint) {
7866        return Err(host::type_error(
7867            "Cannot convert a BigInt value to a number",
7868        ));
7869    }
7870    // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7871    // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7872    // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7873    // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7874    // variadic, and coercing only the first few silently DROPPED the rest —
7875    // `Math.max(...gen)` over five values answered for four of them.
7876    let mut coerced = Vec::with_capacity(args.len());
7877    for a in args {
7878        if matches!(a, Value::Undef) {
7879            coerced.push(a.clone());
7880            continue;
7881        }
7882        let p = host::to_primitive(a, "number")?;
7883        coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7884    }
7885    let args: &[Value] = &coerced;
7886    let x = arg_num(args, 0);
7887    let r = match fname {
7888        "floor" => x.floor(),
7889        "ceil" => x.ceil(),
7890        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7891        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7892        // before the floor sees it, so it answers 1 for the largest double below
7893        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7894        // perturbs integers above 2^52, where `x + 0.5` is no longer
7895        // representable (`Math.round(4503599627370497)` must be the input).
7896        // Splitting the zero-band cases out first also carries the signed zero
7897        // the spec asks for without a post-hoc patch.
7898        "round" => {
7899            if !x.is_finite() || x == 0.0 {
7900                x
7901            } else if x > 0.0 && x < 0.5 {
7902                0.0
7903            } else if (-0.5..0.0).contains(&x) {
7904                -0.0
7905            } else {
7906                // |x| >= 0.5, so `floor` and the subtraction are both exact
7907                // (every double >= 2^52 is already an integer and yields 0 here).
7908                let f = x.floor();
7909                if x - f >= 0.5 {
7910                    f + 1.0
7911                } else {
7912                    f
7913                }
7914            }
7915        }
7916        "trunc" => x.trunc(),
7917        "abs" => x.abs(),
7918        "sign" => {
7919            if x.is_nan() {
7920                f64::NAN
7921            } else if x > 0.0 {
7922                1.0
7923            } else if x < 0.0 {
7924                -1.0
7925            } else {
7926                x
7927            }
7928        }
7929        "sqrt" => x.sqrt(),
7930        "cbrt" => x.cbrt(),
7931        "exp" => x.exp(),
7932        "log" => x.ln(),
7933        "log2" => x.log2(),
7934        "log10" => x.log10(),
7935        "sin" => x.sin(),
7936        "cos" => x.cos(),
7937        "tan" => x.tan(),
7938        "asin" => x.asin(),
7939        "acos" => x.acos(),
7940        "atan" => x.atan(),
7941        "atan2" => x.atan2(arg_num(args, 1)),
7942        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7943        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7944        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7945        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7946        // clause is shared.
7947        "pow" => js_pow(x, arg_num(args, 1)),
7948        // Hyperbolics and the two precision-preserving log/exp forms.
7949        "sinh" => x.sinh(),
7950        "cosh" => x.cosh(),
7951        "tanh" => x.tanh(),
7952        "asinh" => x.asinh(),
7953        "acosh" => x.acosh(),
7954        "atanh" => x.atanh(),
7955        "log1p" => x.ln_1p(),
7956        "expm1" => x.exp_m1(),
7957        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7958        // wraparound, reinterpret as a signed 32-bit result.
7959        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7960        "hypot" => {
7961            // Scale by the largest magnitude before squaring — this avoids the
7962            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7963            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7964            let mut max = 0.0f64;
7965            for x in &xs {
7966                if x.abs() > max {
7967                    max = x.abs();
7968                }
7969            }
7970            if xs.iter().any(|x| x.is_infinite()) {
7971                f64::INFINITY
7972            } else if max == 0.0 || !max.is_finite() {
7973                max
7974            } else {
7975                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7976                max * s.sqrt()
7977            }
7978        }
7979        "random" => pseudo_random(),
7980        "max" => {
7981            if args.is_empty() {
7982                f64::NEG_INFINITY
7983            } else {
7984                let mut m = f64::NEG_INFINITY;
7985                for a in args {
7986                    let n = with_host(|h| h.to_number(a));
7987                    if n.is_nan() {
7988                        return Ok(Value::Float(f64::NAN));
7989                    }
7990                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7991                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
7992                    // must not keep the -0 the first iteration installed.
7993                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
7994                        m = n;
7995                    }
7996                }
7997                m
7998            }
7999        }
8000        "min" => {
8001            if args.is_empty() {
8002                f64::INFINITY
8003            } else {
8004                let mut m = f64::INFINITY;
8005                for a in args {
8006                    let n = with_host(|h| h.to_number(a));
8007                    if n.is_nan() {
8008                        return Ok(Value::Float(f64::NAN));
8009                    }
8010                    // Mirror of `max`: -0 ranks below +0 even though `<` says
8011                    // they are equal, so `Math.min(0, -0)` is -0.
8012                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8013                        m = n;
8014                    }
8015                }
8016                m
8017            }
8018        }
8019        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8020        "clz32" => {
8021            let u = if x.is_finite() {
8022                x.trunc().rem_euclid(4294967296.0) as u32
8023            } else {
8024                0
8025            };
8026            u.leading_zeros() as f64
8027        }
8028        // Round to the nearest single-precision float.
8029        "fround" => (x as f32) as f64,
8030        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8031    };
8032    Ok(Value::Float(r))
8033}
8034
8035/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8036/// Node by nature; kept simple).
8037fn pseudo_random() -> f64 {
8038    use std::cell::Cell;
8039    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8040    SEED.with(|s| {
8041        let mut x = s.get();
8042        x ^= x << 13;
8043        x ^= x >> 7;
8044        x ^= x << 17;
8045        s.set(x);
8046        (x >> 11) as f64 / (1u64 << 53) as f64
8047    })
8048}
8049
8050// ── Object.* ──────────────────────────────────────────────────────────────────
8051
8052/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8053/// properties (10.4.3 `StringExoticObject`).
8054///
8055/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8056/// an exotic object whose own keys are its code-unit indices plus `length`;
8057/// this is the descriptor half of that. (The KEY half lives in
8058/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8059/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8060///
8061/// A boxed `String` object is deliberately NOT routed here: it can carry
8062/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8063/// existing path already reports them alongside the indices.
8064fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8065    // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8066    // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8067    // never catches one.
8068    let s = match v {
8069        Value::Str(s) => (**s).clone(),
8070        _ => with_host(|h| match h.get(v) {
8071            Some(JsObj::Str(s)) => Some(s.clone()),
8072            _ => None,
8073        })?,
8074    };
8075    let units = crate::utf16::Units::of(&s);
8076    Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8077}
8078
8079fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8080    let v = arg0(&args);
8081    require_object_coercible(&v)?;
8082    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8083    // reports every own STRING key the trap named; the enumerating modes
8084    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8085    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8086        if mode == 3 {
8087            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8088            return Ok(with_host(|h| {
8089                let out: Vec<Value> = keys
8090                    .into_iter()
8091                    .filter(|k| !host::is_symbol_key(k))
8092                    .map(|k| h.new_str(k))
8093                    .collect();
8094                h.new_array(out)
8095            }));
8096        }
8097        // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8098        // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8099        // Going through `own_enum_entries` fired the `get` trap once per key, so
8100        // the observable trap sequence carried a trailing `get` node does not
8101        // emit, and a trap with side effects ran when it should not have.
8102        if mode == 0 {
8103            let keys = crate::proxy::own_enum_string_keys(&v)?;
8104            return Ok(with_host(|h| {
8105                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8106                h.new_array(out)
8107            }));
8108        }
8109        let entries = crate::proxy::own_enum_entries(&v)?;
8110        return Ok(with_host(|h| {
8111            let out: Vec<Value> = entries
8112                .into_iter()
8113                .map(|(k, val)| match mode {
8114                    0 => h.new_str(k),
8115                    1 => val,
8116                    _ => {
8117                        let ks = h.new_str(k);
8118                        h.new_array(vec![ks, val])
8119                    }
8120                })
8121                .collect();
8122            h.new_array(out)
8123        }));
8124    }
8125    // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8126    // .prototype`, `String.prototype`, the error hierarchy — answers from the
8127    // generated table too. It was answering from its own property map instead,
8128    // which carries neither the right names nor V8's order: `Symbol.prototype`
8129    // reported `toLocaleString` and omitted `description`, and
8130    // `String.prototype` omitted `length` and every Annex B HTML method.
8131    //
8132    // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8133    // the two representations of a prototype cannot answer differently.
8134    let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8135        .filter(|ns| intrinsic_proto_members(ns).is_some());
8136    // A builtin prototype namespace that exposes enumerable methods for copying
8137    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8138    if let Some(ns) = real_proto_ns.or_else(|| {
8139        with_host(|h| match h.get(&v) {
8140            Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8141            _ => None,
8142        })
8143    }) {
8144        // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8145        // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8146        // interface, which the table records per name.
8147        if let Some(members) = intrinsic_proto_members(&ns) {
8148            let ctor = ns.trim_end_matches(".prototype");
8149            let mut names: Vec<String> = members
8150                .iter()
8151                .filter(|m| mode == 3 || m.starts_with('+'))
8152                .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8153                // `getOwnPropertyNames` reports STRING keys only; the table's
8154                // `@@` entries are symbol-keyed members and belong to
8155                // `getOwnPropertySymbols` instead.
8156                .filter(|m| !m.starts_with("@@"))
8157                .collect();
8158            // Plus whatever a script patched onto this prototype under a NEW
8159            // name — an ordinary enumerable own property, so it lists in every
8160            // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8161            // an assignment that `Array.prototype.patch` read back happily.
8162            //
8163            // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8164            // that member's attributes alone: restoring a saved `join` must not
8165            // turn it into an enumerable key.
8166            for k in with_host(|h| h.builtin_static_keys(&ns)) {
8167                if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8168                    names.push(k);
8169                }
8170            }
8171            return Ok(with_host(|h| {
8172                let out: Vec<Value> = names
8173                    .iter()
8174                    .map(|name| {
8175                        // An accessor member has no thunk — `Map.prototype.size`
8176                        // is not a function — so a VALUE read of one answers
8177                        // undefined rather than synthesizing a callable.
8178                        let val = |h: &mut host::JsHost| {
8179                            if let Some(v) = h.builtin_static(&ns, name) {
8180                                return v;
8181                            }
8182                            let key = format!("@proto:{ctor}:{name}");
8183                            if builtin_meta(&key).is_some() {
8184                                h.alloc(JsObj::Builtin(key))
8185                            } else {
8186                                Value::Undef
8187                            }
8188                        };
8189                        match mode {
8190                            1 => val(h),
8191                            2 => {
8192                                let ks = h.new_str(name.clone());
8193                                let v = val(h);
8194                                h.new_array(vec![ks, v])
8195                            }
8196                            _ => h.new_str(name.clone()),
8197                        }
8198                    })
8199                    .collect();
8200                h.new_array(out)
8201            }));
8202        }
8203        if let Some(names) = builtin_proto_method_names(&ns) {
8204            return Ok(with_host(|h| {
8205                let out: Vec<Value> = names
8206                    .iter()
8207                    .map(|name| match mode {
8208                        1 => h.alloc(JsObj::Builtin(format!(
8209                            "@proto:{}:{name}",
8210                            ns.trim_end_matches(".prototype")
8211                        ))),
8212                        2 => {
8213                            let ks = h.new_str(*name);
8214                            let val = h.alloc(JsObj::Builtin(format!(
8215                                "@proto:{}:{name}",
8216                                ns.trim_end_matches(".prototype")
8217                            )));
8218                            h.new_array(vec![ks, val])
8219                        }
8220                        _ => h.new_str(*name),
8221                    })
8222                    .collect();
8223                h.new_array(out)
8224            }));
8225        }
8226        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8227        // keys are the members node-js implements, each resolved to the same
8228        // first-class value a property read would give.
8229        let mut names = crate::stdlib::namespace_keys(&ns);
8230        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8231        // its members live in the builtin dispatch table. They are
8232        // non-enumerable in V8, so they surface only under
8233        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8234        if names.is_empty() && mode == 3 {
8235            let prefix = format!("{ns}.");
8236            // A builtin constructor's own `length`/`name`/`prototype` come
8237            // first, as they do in V8.
8238            if is_builtin_ctor(&ns) {
8239                names.extend(["length", "name", "prototype"].map(str::to_string));
8240            }
8241            names.extend(
8242                NS_METHODS
8243                    .iter()
8244                    .filter_map(|q| q.strip_prefix(&prefix))
8245                    .map(|m| m.to_string()),
8246            );
8247            // The numeric constants are members too. Without them
8248            // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8249            // actually answers — the eight it dropped being `PI` and its
8250            // siblings, which read fine and now own a descriptor as well.
8251            names.extend(
8252                namespace_constants(&ns)
8253                    .iter()
8254                    .map(|(k, _)| (*k).to_string()),
8255            );
8256        }
8257        // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8258        // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8259        // was `[]`, which said the function had no properties at all while both
8260        // of them read back a value. `length` is listed only where the intrinsic
8261        // table has an arity, so the names never advertise a read that answers
8262        // `undefined`.
8263        if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8264            if builtin_meta(&ns).is_some() {
8265                names.push("length".to_string());
8266            }
8267            names.push("name".to_string());
8268        }
8269        // Whatever a script assigned onto the namespace, in assignment order and
8270        // after the built-in members — an ordinary enumerable own property, so
8271        // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8272        // missing from every listing, which made a patched prototype read as
8273        // unpatched to any code that enumerates rather than reads.
8274        for k in with_host(|h| h.builtin_static_keys(&ns)) {
8275            if !names.contains(&k) {
8276                names.push(k);
8277            }
8278        }
8279        if !names.is_empty() {
8280            let entries: Vec<(String, Value)> = names
8281                .into_iter()
8282                .map(|k| {
8283                    let val = namespace_property(&ns, &k);
8284                    (k, val)
8285                })
8286                .collect();
8287            return Ok(with_host(|h| {
8288                let out: Vec<Value> = entries
8289                    .into_iter()
8290                    .map(|(k, val)| match mode {
8291                        1 => val,
8292                        2 => {
8293                            let ks = h.new_str(k);
8294                            h.new_array(vec![ks, val])
8295                        }
8296                        _ => h.new_str(k),
8297                    })
8298                    .collect();
8299                h.new_array(out)
8300            }));
8301        }
8302    }
8303    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8304    // non-enumerable ones, plus the exotic `length` an array carries.
8305    let entries: Vec<(String, Value)> = with_host(|h| {
8306        if mode == 3 {
8307            // An array's exotic `length` is already placed (after the indices,
8308            // before the ordinary string keys) by `own_key_names`.
8309            return h
8310                .own_key_names(&v, false)
8311                .into_iter()
8312                .map(|k| (k, Value::Undef))
8313                .collect();
8314        }
8315        Vec::new()
8316    });
8317    // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8318    // key/value pairs, so asking it for them ran every enumerable getter —
8319    // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8320    // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8321    // side effects:
8322    //
8323    //     let n = 0; const o = { get g() { n++; return 1 } };
8324    //     Object.keys(o); n   // was 1, node says 0
8325    //
8326    // `values`/`entries` (modes 1 and 2) do read, and still do.
8327    let entries = match mode {
8328        3 => entries,
8329        0 => with_host(|h| h.own_enum_key_names(&v))
8330            .into_iter()
8331            .map(|k| (k, Value::Undef))
8332            .collect(),
8333        _ => host::own_enum_entries_deep(&v)?,
8334    };
8335    Ok(with_host(|h| {
8336        let out: Vec<Value> = entries
8337            .into_iter()
8338            .map(|(k, val)| match mode {
8339                0 | 3 => h.new_str(k),
8340                1 => val,
8341                _ => {
8342                    let ks = h.new_str(k);
8343                    h.new_array(vec![ks, val])
8344                }
8345            })
8346            .collect();
8347        h.new_array(out)
8348    }))
8349}
8350
8351fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8352    let target = arg0(&args);
8353    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8354    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8355    require_object_coercible(&target)?;
8356    for src in args.iter().skip(1) {
8357        // `Object.assign` copies own *enumerable* properties, running any getter
8358        // — symbol-keyed ones included (7.3.25).
8359        let entries = host::own_enum_entries_deep(src)?;
8360        let syms = with_host(|h| h.own_symbol_entries(src));
8361        // A plain object target is filled in place (one borrow, then a single
8362        // re-canonicalization of the integer-index keys).
8363        let filled = with_host(|h| {
8364            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8365                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8366                    p.insert(k, v);
8367                }
8368                host::canonicalize_own_keys(p);
8369                return true;
8370            }
8371            false
8372        });
8373        // Any OTHER target — an array being the common one — goes through the
8374        // ordinary Set path. The in-place branch above matched `JsObj::Object`
8375        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8376        // returned the untouched array: no error, just a missing property. The
8377        // Set path is what an `arr.extra = 9` assignment already used, so index
8378        // and non-index keys land where they do for a direct write.
8379        if !filled {
8380            for (k, v) in entries.into_iter().chain(syms) {
8381                set_property(&target, &k, v)?;
8382            }
8383        }
8384    }
8385    Ok(target)
8386}
8387
8388fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8389    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8390    let mut props: IndexMap<String, Value> = IndexMap::new();
8391    for p in pairs {
8392        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8393        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8394        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8395        props.insert(key, val);
8396    }
8397    Ok(with_host(|h| h.new_object(props)))
8398}
8399
8400/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8401/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8402/// members mapped to that key, in first-seen key order.
8403fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8404    group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8405    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8406    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8407    // Stepped, not drained: the callback runs per element, so a throwing one
8408    // stops at the first. Draining first meant an infinite source never reached
8409    // the callback at all and the call HUNG.
8410    host::iter_for_each(&arg0(&args), |item, i| {
8411        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8412        let key = with_host(|h| h.property_key(&key_v));
8413        groups.entry(key).or_default().push(item);
8414        Ok(())
8415    })?;
8416    let props: IndexMap<String, Value> = with_host(|h| {
8417        groups
8418            .into_iter()
8419            .map(|(k, v)| (k, h.new_array(v)))
8420            .collect()
8421    });
8422    let obj = with_host(|h| h.new_object(props));
8423    // A null-prototype object (as Node returns), so it has no inherited members.
8424    with_host(|h| {
8425        let nv = h.null();
8426        h.set_proto(&obj, nv);
8427    });
8428    Ok(obj)
8429}
8430
8431/// The `groupBy` family words a non-iterable argument its OWN way — a third
8432/// vocabulary, alongside the array-literal spread's and the call spread's:
8433///
8434/// ```text
8435/// null / undefined   "<Name> called on null or undefined"
8436/// anything else      "<typeof> [value ]is not iterable (cannot read property
8437///                     Symbol(Symbol.iterator))"
8438/// ```
8439///
8440/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8441/// string and a boolean name the value too.
8442fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8443    if with_host(|h| h.is_nullish(v)) {
8444        return Err(host::type_error(&format!(
8445            "{name} called on null or undefined"
8446        )));
8447    }
8448    // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8449    // here, so the stepping loop below then saw an exhausted one — the finite
8450    // case returned an empty group and the infinite case was back to hanging.
8451    let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8452    if with_host(|h| host::is_callable(h, &iter_fn)) {
8453        return Ok(());
8454    }
8455    Err(host::type_error(&not_iterable_typed(v)))
8456}
8457
8458/// The `<type> <value> is not iterable (cannot read property
8459/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8460/// no name to report: a plain object, a symbol and a bigint name only their
8461/// TYPE; a number, a string and a boolean name the value too.
8462pub(crate) fn not_iterable_typed(v: &Value) -> String {
8463    let shown = with_host(|h| {
8464        let kind = h.type_of(v);
8465        match kind {
8466            "object" | "symbol" | "bigint" => kind.to_string(),
8467            "string" => format!("string \"{}\"", h.str_of(v)),
8468            _ => format!("{kind} {}", h.str_of(v)),
8469        }
8470    });
8471    format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8472}
8473
8474/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8475/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8476fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8477    group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8478    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8479    let m = with_host(|h| {
8480        h.alloc(JsObj::Map {
8481            entries: IndexMap::new(),
8482            weak: false,
8483        })
8484    });
8485    // Stepped for the same reason `Object.groupBy` is.
8486    host::iter_for_each(&arg0(&args), |item, i| {
8487        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8488        let existing = map_method(&m, "get", vec![key_v.clone()])?;
8489        if matches!(existing, Value::Undef) {
8490            let arr = with_host(|h| h.new_array(vec![item]));
8491            map_method(&m, "set", vec![key_v, arr])?;
8492        } else {
8493            with_host(|h| {
8494                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8495                    a.push(item);
8496                }
8497            });
8498        }
8499        Ok(())
8500    })?;
8501    Ok(m)
8502}
8503
8504/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8505/// element and each `mapFn` result.
8506///
8507/// Written in JavaScript and compiled once, because the operation IS an async
8508/// function: a Rust builtin runs outside any coroutine and has no way to await,
8509/// so draining a promise from there would mean running the microtask queue by
8510/// hand. Delegating to the engine's own `async`/`for await` keeps the
8511/// suspension semantics — and the ordering they imply — exactly the language's.
8512///
8513/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8514/// array-like. Everything iterable goes through `for await`, which awaits a sync
8515/// source's elements individually — that is what makes
8516/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8517/// accepted because an async generator object does not expose
8518/// `Symbol.asyncIterator` on this frontend.
8519fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8520    thread_local! {
8521        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8522    }
8523    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8524        const out = []; let i = 0;\n\
8525        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8526        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8527            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8528        if (iterable) {\n\
8529            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8530            return out;\n\
8531        }\n\
8532        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8533        while (i < len) { await step(items[i]); }\n\
8534        return out;\n\
8535    })";
8536    let f = IMPL.with(|c| c.borrow().clone());
8537    let f = match f {
8538        Some(f) => f,
8539        None => {
8540            let f = crate::eval_in_global_scope(SRC)?;
8541            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8542            f
8543        }
8544    };
8545    host::invoke(&f, args, None)
8546}
8547
8548fn array_from(args: Vec<Value>) -> Result<Value, String> {
8549    // `Array.from` accepts generators and user iterables, plus array-likes with a
8550    // numeric `.length`.
8551    let src = arg0(&args);
8552    if let Some(cb) = args.get(1).cloned() {
8553        // Stepped, not drained: the mapper runs per element as the iterator
8554        // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8555        // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8556        // all and HUNG, and a throwing mapper could not close the iterator.
8557        let this = this_arg(&args, 2);
8558        let mut out = Vec::new();
8559        let mapped = host::iter_for_each(&src, |v, i| {
8560            out.push(host::invoke(
8561                &cb,
8562                vec![v, Value::Float(i as f64)],
8563                this.clone(),
8564            )?);
8565            Ok(())
8566        });
8567        match mapped {
8568            Ok(()) => {}
8569            // An array-LIKE has no iterator; fall back to its indexed items.
8570            Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8571                out.clear();
8572                for (i, it) in array_like_items(&src).into_iter().enumerate() {
8573                    out.push(host::invoke(
8574                        &cb,
8575                        vec![it, Value::Float(i as f64)],
8576                        this.clone(),
8577                    )?);
8578                }
8579            }
8580            Err(e) => return Err(e),
8581        }
8582        return construct_array_like(host::current_static_this(), out);
8583    }
8584    let items = match host::iter_all(&src) {
8585        Ok(v) => v,
8586        Err(_) => array_like_items(&src),
8587    };
8588    // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8589    // result is an instance of it. It always allocated a plain array, which is
8590    // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8591    // never started.
8592    construct_array_like(host::current_static_this(), items)
8593}
8594
8595/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8596pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8597    // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8598    // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8599    // because the infallible read does no `ToPrimitive`. A throw from it is
8600    // swallowed here for the same reason the `length` read is: this helper has
8601    // no way to report one, and every caller treats an unreadable length as 0.
8602    let len = get_property(src, "length")
8603        .ok()
8604        .and_then(|l| host::to_primitive(&l, "number").ok())
8605        .map(|l| with_host(|h| h.to_number(&l)))
8606        .unwrap_or(0.0);
8607    if !len.is_finite() || len <= 0.0 {
8608        return Vec::new();
8609    }
8610    (0..len as usize)
8611        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8612        .collect()
8613}
8614
8615// ── JSON ──────────────────────────────────────────────────────────────────────
8616
8617fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8618    // A CALLABLE second argument is the replacer function, and it is checked
8619    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8620    // callable never also reaches the key-filter path below.
8621    let replacer = args
8622        .get(1)
8623        .filter(|r| with_host(|h| host::is_callable(h, r)))
8624        .cloned();
8625    // `toJSON` and the replacer run BEFORE serialization and are user code, so
8626    // the tree is rewritten first — outside the host borrow `json_str` holds,
8627    // and before the BigInt walk, which has no cycle guard of its own.
8628    //
8629    // The top-level value is a property of a synthetic wrapper `{ "": value }`
8630    // under key `""`, which is exactly the holder the replacer receives as
8631    // `this` on its first call.
8632    let root = arg0(&args);
8633    let wrapper = with_host(|h| {
8634        let mut m: IndexMap<String, Value> = IndexMap::new();
8635        m.insert(String::new(), root.clone());
8636        h.new_object(m)
8637    });
8638    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8639    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8640    // bigint form), matching Node's exact message.
8641    if with_host(|h| json_has_bigint(h, &v)) {
8642        return Err(host::type_error("Do not know how to serialize a BigInt"));
8643    }
8644    let indent = match args.get(2) {
8645        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8646        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8647        None => String::new(),
8648    };
8649    // A replacer array (args[1]) restricts which object keys are serialized.
8650    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8651        with_host(|h| match h.get(r) {
8652            Some(JsObj::Array(items)) => {
8653                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8654            }
8655            _ => None,
8656        })
8657    });
8658    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8659    match s {
8660        Some(s) => Ok(with_host(|h| h.new_str(s))),
8661        None => Ok(Value::Undef),
8662    }
8663}
8664
8665/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8666/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8667/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8668/// object survives. Applies to user methods, class methods, and the native
8669/// `Date`/`Buffer`/`URL` accessors alike.
8670///
8671/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8672/// objects currently being walked so a cyclic structure is reported rather than
8673/// spinning forever.
8674///
8675/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8676/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8677/// because the inner method is a plain (unserializable) function property of the
8678/// returned object, not a second conversion hook.
8679fn apply_to_json(
8680    holder: &Value,
8681    key: &str,
8682    v: &Value,
8683    path: &mut JsonPath,
8684    rep: Option<&Value>,
8685) -> Result<Value, String> {
8686    let mut v = v.clone();
8687    if matches!(v, Value::Obj(_)) {
8688        let tag = crate::stdlib::native_tag(&v);
8689        // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8690        // supplies one through its `get` trap. `lookup_chain` walks the
8691        // property map and never asks the handler, so a proxy carrying a
8692        // `toJSON` was serialized as a plain object instead of by its own
8693        // method — and node's trap log starts with that `get`.
8694        let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8695            get_property(&v, "toJSON")?
8696        } else {
8697            with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8698        };
8699        let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8700            || tag
8701                .as_deref()
8702                .map(crate::stdlib::has_to_json)
8703                .unwrap_or(false);
8704        if has_to_json {
8705            let k = with_host(|h| h.new_str(key.to_string()));
8706            v = host::call_method(&v, "toJSON", vec![k])?;
8707        }
8708    }
8709    if let Some(rep) = rep {
8710        let k = with_host(|h| h.new_str(key.to_string()));
8711        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8712    }
8713    // How `v` was reached from its holder, as V8 names the step in a
8714    // circular-structure message: `index 1` under an array, else `property 'k'`.
8715    let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8716        format!("index {key}")
8717    } else {
8718        format!("property '{key}'")
8719    };
8720    json_walk_children(&v, path, &via, rep)
8721}
8722
8723/// The objects `JSON.stringify` is inside of, outermost first, each with the
8724/// step that reached it from its holder (`property 'x'` / `index 1`).
8725type JsonPath = Vec<(String, Value)>;
8726
8727/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8728/// starts at to the key that closes it. At most the first two and the last one
8729/// intermediate step are listed, with `|     ...` standing for the rest.
8730fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8731    const PREFIX: usize = 2;
8732    const POSTFIX: usize = 1;
8733    let ctor = |v: &Value| -> String {
8734        with_host(|h| match h.get(v) {
8735            Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8736            _ => match h.ctor_name(v) {
8737                n if n.is_empty() => "Object".to_string(),
8738                n => n,
8739            },
8740        })
8741    };
8742    let line = |i: usize| {
8743        format!(
8744            "\n    |     {} -> object with constructor '{}'",
8745            path[i].0,
8746            ctor(&path[i].1)
8747        )
8748    };
8749    let mut msg = format!(
8750        "Converting circular structure to JSON\n    --> starting at object with constructor '{}'",
8751        ctor(&path[start].1)
8752    );
8753    let prefix_end = path.len().min(start + 1 + PREFIX);
8754    for i in start + 1..prefix_end {
8755        msg.push_str(&line(i));
8756    }
8757    if path.len() > prefix_end + POSTFIX {
8758        msg.push_str("\n    |     ...");
8759    }
8760    for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8761        msg.push_str(&line(i));
8762    }
8763    msg.push_str(&format!("\n    --- {closing} closes the circle"));
8764    msg
8765}
8766
8767/// Whether a raw property key of a host object is one `json_str` serializes. The
8768/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8769/// invisible to JSON, so the replacer must not be invoked for them either.
8770fn json_visible_key(k: &str) -> bool {
8771    !k.starts_with("@@") && !k.starts_with('#')
8772}
8773
8774/// Recurse into the elements/properties of an already-converted value, running
8775/// `apply_to_json` for each with this value as the holder.
8776fn json_walk_children(
8777    v: &Value,
8778    path: &mut JsonPath,
8779    via: &str,
8780    rep: Option<&Value>,
8781) -> Result<Value, String> {
8782    if !matches!(v, Value::Obj(_)) {
8783        return Ok(v.clone());
8784    }
8785    // A value that contains itself has no JSON form.
8786    if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8787        return Err(host::type_error(&circular_json_message(path, start, via)));
8788    }
8789    // A Proxy owns no property map, so it is snapshotted through its traps into
8790    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8791    // — which read every member through `[[Get]]`, exactly as the snapshot does.
8792    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8793        let snap = crate::proxy::json_snapshot(v)?;
8794        path.push((via.to_string(), v.clone()));
8795        let out = json_walk_children(&snap, path, via, rep);
8796        path.pop();
8797        return out;
8798    }
8799    let obj = with_host(|h| h.get(v).cloned());
8800    path.push((via.to_string(), v.clone()));
8801    let out = (|| match obj {
8802        Some(JsObj::Array(items)) => {
8803            // Read the elements through the accessor-aware funnel: an index
8804            // with a getter must be SERIALIZED as what the getter returns, and
8805            // the backing vector still holds the stale slot.
8806            let mut resolved = items;
8807            // An index with a getter must be SERIALIZED as what the getter
8808            // returns, and it also forces a rebuild below: keeping the original
8809            // array would hand the serializer back the stale backing vector.
8810            let had_accessor = resolve_index_accessors(v, &mut resolved);
8811            let items = resolved;
8812            let mut out = Vec::with_capacity(items.len());
8813            let mut changed = had_accessor;
8814            for (i, it) in items.iter().enumerate() {
8815                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8816                changed |= !with_host(|h| h.strict_eq(&nv, it));
8817                out.push(nv);
8818            }
8819            // Keep identity when nothing changed, so an enclosing object is not
8820            // needlessly rebuilt (which would drop its property attributes).
8821            if changed {
8822                Ok(with_host(|h| h.new_array(out)))
8823            } else {
8824                Ok(v.clone())
8825            }
8826        }
8827        Some(JsObj::Object(props)) => {
8828            // An enumerable own accessor must have its getter RUN and the result
8829            // serialized. That cannot happen inside `json_str` (which holds the
8830            // host borrow), so materialize here — the same reason `toJSON` is
8831            // applied in this pass.
8832            let has_accessor = with_host(|h| {
8833                h.own_accessor_keys(v)
8834                    .iter()
8835                    .any(|k| h.prop_attrs(v, k).enumerable)
8836            });
8837            if has_accessor {
8838                let mut next: IndexMap<String, Value> = IndexMap::new();
8839                for (k, val) in host::own_enum_entries_deep(v)? {
8840                    let nv = if json_visible_key(&k) {
8841                        apply_to_json(v, &k, &val, path, rep)?
8842                    } else {
8843                        val
8844                    };
8845                    next.insert(k, nv);
8846                }
8847                return Ok(with_host(|h| h.new_object(next)));
8848            }
8849            // Only rebuild when a descendant actually changed, so plain data keeps
8850            // its identity (and its prototype / native tag).
8851            let mut next: IndexMap<String, Value> = IndexMap::new();
8852            let mut changed = false;
8853            for (k, val) in &props {
8854                let nv = if json_visible_key(k) {
8855                    apply_to_json(v, k, val, path, rep)?
8856                } else {
8857                    val.clone()
8858                };
8859                changed |= !with_host(|h| h.strict_eq(&nv, val));
8860                next.insert(k.clone(), nv);
8861            }
8862            if changed {
8863                Ok(with_host(|h| {
8864                    let o = h.new_object(next);
8865                    h.copy_prop_attrs(v, &o);
8866                    o
8867                }))
8868            } else {
8869                Ok(v.clone())
8870            }
8871        }
8872        _ => Ok(v.clone()),
8873    })();
8874    path.pop();
8875    out
8876}
8877
8878/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8879/// try to serialize (a value in an array/object) — such a value throws.
8880fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8881    match h.get(v) {
8882        Some(JsObj::BigInt(_)) => true,
8883        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8884        Some(JsObj::Object(props)) => props
8885            .iter()
8886            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8887            .any(|(_, val)| json_has_bigint(h, val)),
8888        _ => false,
8889    }
8890}
8891
8892fn json_str(
8893    h: &host::JsHost,
8894    v: &Value,
8895    indent: &str,
8896    depth: usize,
8897    keys: Option<&[String]>,
8898) -> Option<String> {
8899    let sep = if indent.is_empty() { ":" } else { ": " };
8900    match v {
8901        Value::Undef => None,
8902        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8903        Value::Int(n) => Some(n.to_string()),
8904        Value::Float(f) => Some(if f.is_finite() {
8905            host::fmt_number(*f)
8906        } else {
8907            "null".into()
8908        }),
8909        Value::Str(s) => Some(json_quote(s)),
8910        Value::Obj(_) => match h.get(v) {
8911            Some(JsObj::Str(s)) => Some(json_quote(s)),
8912            Some(JsObj::Null) => Some("null".into()),
8913            // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8914            // whole point of it, and it is why a number wider than a `double`
8915            // can survive a round trip.
8916            _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8917                Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8918                _ => None,
8919            },
8920            // A Map/Set has no ENTRIES to serialize (they are internal slots),
8921            // but any own property a script attached is serialized like an
8922            // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8923            // is `{"a":1}`.
8924            Some(JsObj::Map { .. })
8925            | Some(JsObj::Set { .. })
8926            | Some(JsObj::RegExp(_))
8927            // A Promise and a generator are ORDINARY objects to the serializer:
8928            // their state is internal slots, so they contribute no entries and
8929            // render as `{}`. They were being omitted entirely instead, so a
8930            // promise in an array became `null` and one in an object vanished.
8931            | Some(JsObj::Promise { .. })
8932            | Some(JsObj::Generator { .. }) => {
8933                let parts: Vec<String> = h
8934                    .own_enum_entries(v)
8935                    .into_iter()
8936                    .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8937                    .filter_map(|(k, val)| {
8938                        json_str(h, &val, indent, depth + 1, keys)
8939                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8940                    })
8941                    .collect();
8942                Some(wrap(&parts, "{", "}", indent, depth))
8943            }
8944            // A NON-callable builtin is a namespace object, not a function, so
8945            // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8946            // are all non-enumerable), where omitting it made the whole property
8947            // disappear from its holder.
8948            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8949                let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8950                    .into_iter()
8951                    .filter_map(|k| {
8952                        let val = h.builtin_static(n, &k)?;
8953                        json_str(h, &val, indent, depth + 1, keys)
8954                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8955                    })
8956                    .collect();
8957                Some(wrap(&parts, "{", "}", indent, depth))
8958            }
8959            // Functions and symbols are omitted (undefined) as values.
8960            Some(JsObj::Func(_))
8961            | Some(JsObj::Builtin(_))
8962            | Some(JsObj::BoundMethod { .. })
8963            | Some(JsObj::BoundFunc { .. })
8964            | Some(JsObj::Class(_))
8965            | Some(JsObj::Symbol { .. }) => None,
8966            Some(JsObj::Array(items)) => {
8967                if items.is_empty() {
8968                    return Some("[]".into());
8969                }
8970                let parts: Vec<String> = items
8971                    .iter()
8972                    .map(|x| {
8973                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8974                    })
8975                    .collect();
8976                Some(wrap(&parts, "[", "]", indent, depth))
8977            }
8978            Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8979                // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8980                // the primitive it boxes, not as the object holding it —
8981                // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8982                json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8983            }
8984            Some(JsObj::Object(props)) => {
8985                // A replacer array restricts (and orders) which keys are emitted.
8986                let parts: Vec<String> = match keys {
8987                    Some(allow) => allow
8988                        .iter()
8989                        .filter_map(|k| {
8990                            props.get(k).and_then(|val| {
8991                                json_str(h, val, indent, depth + 1, keys)
8992                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8993                            })
8994                        })
8995                        .collect(),
8996                    None => h
8997                        .own_enum_entries(v)
8998                        .iter()
8999                        .filter_map(|(k, val)| {
9000                            json_str(h, val, indent, depth + 1, keys)
9001                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9002                        })
9003                        .collect(),
9004                };
9005                if parts.is_empty() {
9006                    return Some("{}".into());
9007                }
9008                Some(wrap(&parts, "{", "}", indent, depth))
9009            }
9010            _ => Some("null".into()),
9011        },
9012        _ => Some("null".into()),
9013    }
9014}
9015
9016fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9017    if indent.is_empty() {
9018        format!("{open}{}{close}", parts.join(","))
9019    } else {
9020        let pad = indent.repeat(depth + 1);
9021        let pad_close = indent.repeat(depth);
9022        format!(
9023            "{open}\n{pad}{}\n{pad_close}{close}",
9024            parts.join(&format!(",\n{pad}"))
9025        )
9026    }
9027}
9028
9029fn json_quote(s: &str) -> String {
9030    let mut out = String::from("\"");
9031    for c in s.chars() {
9032        match c {
9033            '"' => out.push_str("\\\""),
9034            '\\' => out.push_str("\\\\"),
9035            '\n' => out.push_str("\\n"),
9036            '\t' => out.push_str("\\t"),
9037            '\r' => out.push_str("\\r"),
9038            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9039            // Backspace and form feed were missing, so they fell through to the
9040            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9041            // `""` where node produces `"\b"`. Both parse back to the same
9042            // string, so the difference is invisible to a round trip and shows
9043            // up only as a byte mismatch against a fixture or a checksum.
9044            '\u{8}' => out.push_str("\\b"),
9045            '\u{c}' => out.push_str("\\f"),
9046            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9047            _ => out.push(c),
9048        }
9049    }
9050    out.push('"');
9051    out
9052}
9053
9054fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9055    let s = with_host(|h| h.str_of(&arg0(&args)));
9056    let mut p = JsonParser {
9057        chars: s.chars().collect(),
9058        pos: 0,
9059        prims: Vec::new(),
9060        record: args
9061            .get(1)
9062            .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9063    };
9064    p.skip_ws();
9065    if p.peek().is_none() {
9066        return Err("SyntaxError: Unexpected end of JSON input".into());
9067    }
9068    let v = p.parse_value()?;
9069    let value_end = p.pos;
9070    p.skip_ws();
9071    // Anything after the top-level value is an error — the parser used to accept
9072    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9073    if let Some(c) = p.peek() {
9074        // V8 names the token kind only when it butts directly against the value
9075        // (`01` -> "Unexpected number at position 1"); with whitespace between
9076        // it is just a non-whitespace character (`1 2`).
9077        // Only a digit butted directly against a completed number literal —
9078        // V8's number scanner is still in number context there. `5"x"` and
9079        // `[0,1]0` exit the scanner cleanly and get the generic message.
9080        let after_number = value_end > 0
9081            && p.pos == value_end
9082            && p.chars[value_end - 1].is_ascii_digit()
9083            && c.is_ascii_digit();
9084        return Err(if after_number {
9085            p.err_at("Unexpected number", p.pos)
9086        } else {
9087            p.err_trailing(p.pos)
9088        });
9089    }
9090    // Optional reviver: walk bottom-up, transforming each (key, value).
9091    if let Some(reviver) = args
9092        .get(1)
9093        .filter(|r| with_host(|h| host::is_callable(h, r)))
9094        .cloned()
9095    {
9096        // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9097        // constructs before the walk.
9098        let root = with_host(|h| {
9099            let mut m: IndexMap<String, Value> = IndexMap::new();
9100            m.insert(String::new(), v.clone());
9101            h.new_object(m)
9102        });
9103        return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9104    }
9105    Ok(v)
9106}
9107
9108/// `JSON.parse` reviver walk: recurse into children first, then call
9109/// `reviver(key, value)`; a returned `undefined` drops the property.
9110///
9111/// The reviver runs with the HOLDER as `this` (25.5.1.1
9112/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9113/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9114/// so `this` was undefined and a reviver could not reach its siblings.
9115/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9116/// VERBATIM, so a number too large for a `double` survives a round trip
9117/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9118///
9119/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9120/// across the whole matrix, is
9121///
9122/// ```text
9123/// ""                 -> SyntaxError: Invalid value for JSON.rawJSON
9124/// leading whitespace -> the parse error for that first character
9125/// a complete literal -> ok
9126/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9127/// a broken literal   -> the parse error the scanner raised
9128/// ```
9129///
9130/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9131/// invalid-value message even though `JSON.parse` accepts the former and gives
9132/// a token error for the latter.
9133fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9134    const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9135    let s = with_host(|h| h.str_of(&arg0(&args)));
9136    if s.is_empty() {
9137        return Err(INVALID.into());
9138    }
9139    let mut p = JsonParser {
9140        chars: s.chars().collect(),
9141        pos: 0,
9142        prims: Vec::new(),
9143        record: false,
9144    };
9145    // An object or an array is rejected where it starts, as leading whitespace
9146    // is — both are "not a primitive", but node reports the token.
9147    if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9148        return Err(p.err_token(0));
9149    }
9150    p.parse_value()?;
9151    if p.pos != p.chars.len() {
9152        // A digit butted against a completed number is still in the number
9153        // scanner, so `"01"` reports the scanner's error rather than leftover
9154        // input — the same distinction `json_parse` draws for trailing text.
9155        if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9156            return Err(p.err_at("Unexpected number", p.pos));
9157        }
9158        return Err(INVALID.into());
9159    }
9160    // A null prototype and one own `rawJSON` property, frozen — the brand is a
9161    // hidden slot so `Object.keys` stays `["rawJSON"]`.
9162    Ok(with_host(|h| {
9163        let mut m: IndexMap<String, Value> = IndexMap::new();
9164        let text = h.new_str(s);
9165        m.insert("rawJSON".into(), text);
9166        let o = h.new_object(m);
9167        let null = h.null();
9168        h.set_proto(&o, null);
9169        h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9170        h.seal_object(&o, true);
9171        o
9172    }))
9173}
9174
9175/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9176/// NOT one, which is why the marker is a hidden slot rather than the property.
9177fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9178    Ok(Value::Bool(is_raw_json(&arg0(&args))))
9179}
9180
9181fn is_raw_json(v: &Value) -> bool {
9182    with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9183}
9184
9185fn json_revive(
9186    key: &str,
9187    val: Value,
9188    reviver: &Value,
9189    holder: &Value,
9190    prims: &[String],
9191    next: &mut usize,
9192) -> Result<Value, String> {
9193    // A PRIMITIVE claims the next recorded source slice before its children
9194    // would — it has none — and a container claims nothing. The walk descends in
9195    // the same order the parse produced them, so one cursor lines the two up.
9196    let is_container =
9197        with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9198    let source = if !is_container {
9199        let s = prims.get(*next).cloned();
9200        if s.is_some() {
9201            *next += 1;
9202        }
9203        s
9204    } else {
9205        None
9206    };
9207    match with_host(|h| h.get(&val).cloned()) {
9208        Some(JsObj::Array(items)) => {
9209            for i in 0..items.len() {
9210                let elem = with_host(|h| match h.get(&val) {
9211                    Some(JsObj::Array(it)) => it[i].clone(),
9212                    _ => Value::Undef,
9213                });
9214                let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9215                with_host(|h| {
9216                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9217                        it[i] = nv;
9218                    }
9219                });
9220            }
9221        }
9222        Some(JsObj::Object(props)) => {
9223            let keys: Vec<String> = props
9224                .keys()
9225                .filter(|k| !k.starts_with("@@"))
9226                .cloned()
9227                .collect();
9228            for k in keys {
9229                let elem = with_host(|h| match h.get(&val) {
9230                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9231                    _ => Value::Undef,
9232                });
9233                let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9234                with_host(|h| {
9235                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9236                        if matches!(nv, Value::Undef) {
9237                            p.shift_remove(&k);
9238                        } else {
9239                            p.insert(k.clone(), nv);
9240                        }
9241                    }
9242                });
9243            }
9244        }
9245        _ => {}
9246    }
9247    let kv = with_host(|h| h.new_str(key.to_string()));
9248    // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9249    // primitive, an empty object for an array or an object — node passes it
9250    // either way, and code reading `ctx.source` used to die on `undefined`
9251    // because only two arguments were passed.
9252    let ctx = with_host(|h| {
9253        let mut m: IndexMap<String, Value> = IndexMap::new();
9254        if let Some(s) = source {
9255            let sv = h.new_str(s);
9256            m.insert("source".into(), sv);
9257        }
9258        h.new_object(m)
9259    });
9260    host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9261}
9262
9263struct JsonParser {
9264    chars: Vec<char>,
9265    pos: usize,
9266    /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9267    /// third argument reports as `context.source` (25.5.1.1). Only collected
9268    /// when a reviver was supplied.
9269    ///
9270    /// A flat list rather than a parallel tree because the reviver walk visits
9271    /// primitives in the same depth-first order the parse produced them, so an
9272    /// index into this is enough to line them up.
9273    prims: Vec<String>,
9274    record: bool,
9275}
9276impl JsonParser {
9277    fn peek(&self) -> Option<char> {
9278        self.chars.get(self.pos).copied()
9279    }
9280
9281    /// `at position N (line L column C)` — the location suffix V8 appends to the
9282    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9283    /// node-js counts `char`s, which agree for the BMP.
9284    fn at(&self, pos: usize) -> String {
9285        let mut line = 1usize;
9286        let mut col = 1usize;
9287        for c in &self.chars[..pos.min(self.chars.len())] {
9288            if *c == '\n' {
9289                line += 1;
9290                col = 1;
9291            } else {
9292                col += 1;
9293            }
9294        }
9295        format!(" at position {pos} (line {line} column {col})")
9296    }
9297
9298    /// A positional error (`Expected ':' after property name in JSON at …`).
9299    fn err_at(&self, what: &str, pos: usize) -> String {
9300        format!("SyntaxError: {what} in JSON{}", self.at(pos))
9301    }
9302
9303    /// The one positional message V8 does NOT suffix with `in JSON`.
9304    fn err_trailing(&self, pos: usize) -> String {
9305        format!(
9306            "SyntaxError: Unexpected non-whitespace character after JSON{}",
9307            self.at(pos)
9308        )
9309    }
9310
9311    /// V8's default parse error: the offending character plus a window of the
9312    /// source. The whole input is quoted when it is short (<= 20 chars);
9313    /// otherwise a 10-character context window either side of `pos` is shown,
9314    /// elided with `...` on whichever side was cut.
9315    fn err_token(&self, pos: usize) -> String {
9316        const MAX_WHOLE: usize = 20;
9317        const CONTEXT: usize = 10;
9318        let len = self.chars.len();
9319        let Some(c) = self.chars.get(pos) else {
9320            return "SyntaxError: Unexpected end of JSON input".into();
9321        };
9322        // V8 reports the whole input for the JS literals that are famously not
9323        // JSON, without naming an offending character.
9324        let whole: String = self.chars.iter().collect();
9325        if matches!(
9326            whole.as_str(),
9327            "undefined" | "NaN" | "Infinity" | "-Infinity"
9328        ) {
9329            return format!("SyntaxError: \"{whole}\" is not valid JSON");
9330        }
9331        let snippet = if len <= MAX_WHOLE {
9332            format!("\"{whole}\"")
9333        } else {
9334            let start = pos.saturating_sub(CONTEXT);
9335            let end = (pos + CONTEXT).min(len);
9336            let body: String = self.chars[start..end].iter().collect();
9337            let head = if start > 0 { "..." } else { "" };
9338            let tail = if end < len { "..." } else { "" };
9339            format!("{head}\"{body}\"{tail}")
9340        };
9341        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9342    }
9343
9344    fn skip_ws(&mut self) {
9345        while matches!(
9346            self.peek(),
9347            Some(' ') | Some('\n') | Some('\t') | Some('\r')
9348        ) {
9349            self.pos += 1;
9350        }
9351    }
9352    fn parse_value(&mut self) -> Result<Value, String> {
9353        self.skip_ws();
9354        let start = self.pos;
9355        let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9356        let v = match self.peek() {
9357            Some('{') => self.parse_object(),
9358            Some('[') => self.parse_array(),
9359            Some('"') => {
9360                let s = self.parse_string()?;
9361                Ok(with_host(|h| h.new_str(s)))
9362            }
9363            Some('t') | Some('f') => self.parse_bool(),
9364            Some('n') => {
9365                self.expect_lit("null")?;
9366                Ok(with_host(|h| h.null()))
9367            }
9368            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9369            None => Err("SyntaxError: Unexpected end of JSON input".into()),
9370            _ => Err(self.err_token(self.pos)),
9371        }?;
9372        if prim && self.record {
9373            self.prims
9374                .push(self.chars[start..self.pos].iter().collect());
9375        }
9376        Ok(v)
9377    }
9378    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9379        for ch in lit.chars() {
9380            match self.peek() {
9381                Some(c) if c == ch => self.pos += 1,
9382                // V8 reports the first character that broke the literal, which is
9383                // why `foo` complains about `'o'` (index 2) and not `'f'`.
9384                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9385                _ => return Err(self.err_token(self.pos)),
9386            }
9387        }
9388        Ok(())
9389    }
9390    fn parse_bool(&mut self) -> Result<Value, String> {
9391        if self.peek() == Some('t') {
9392            self.expect_lit("true")?;
9393            Ok(Value::Bool(true))
9394        } else {
9395            self.expect_lit("false")?;
9396            Ok(Value::Bool(false))
9397        }
9398    }
9399    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9400    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9401    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9402    /// it. Each way the grammar can run out has its own message.
9403    fn parse_number(&mut self) -> Result<Value, String> {
9404        let start = self.pos;
9405        if self.peek() == Some('-') {
9406            self.pos += 1;
9407            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9408                return Err(self.err_at("No number after minus sign", self.pos));
9409            }
9410        }
9411        if self.peek() == Some('0') {
9412            self.pos += 1;
9413        } else {
9414            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9415                self.pos += 1;
9416            }
9417        }
9418        if self.peek() == Some('.') {
9419            self.pos += 1;
9420            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9421                return Err(self.err_at("Unterminated fractional number", self.pos));
9422            }
9423            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9424                self.pos += 1;
9425            }
9426        }
9427        if matches!(self.peek(), Some('e') | Some('E')) {
9428            self.pos += 1;
9429            if matches!(self.peek(), Some('+') | Some('-')) {
9430                self.pos += 1;
9431            }
9432            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9433                return Err(self.err_at("Exponent part is missing a number", self.pos));
9434            }
9435            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9436                self.pos += 1;
9437            }
9438        }
9439        let s: String = self.chars[start..self.pos].iter().collect();
9440        s.parse::<f64>()
9441            .map(Value::Float)
9442            .map_err(|_| self.err_at("Unexpected number", start))
9443    }
9444    fn parse_string(&mut self) -> Result<String, String> {
9445        self.pos += 1; // opening quote
9446        let mut out = String::new();
9447        loop {
9448            match self.peek() {
9449                None => return Err(self.err_at("Unterminated string", self.pos)),
9450                Some('"') => {
9451                    self.pos += 1;
9452                    break;
9453                }
9454                Some('\\') => {
9455                    self.pos += 1;
9456                    match self.peek() {
9457                        Some('n') => out.push('\n'),
9458                        Some('t') => out.push('\t'),
9459                        Some('r') => out.push('\r'),
9460                        Some('"') => out.push('"'),
9461                        Some('\\') => out.push('\\'),
9462                        Some('/') => out.push('/'),
9463                        Some('b') => out.push('\u{08}'),
9464                        Some('f') => out.push('\u{0C}'),
9465                        Some('u') => {
9466                            let h: String = self.chars
9467                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9468                                .iter()
9469                                .collect();
9470                            if let Ok(n) = u32::from_str_radix(&h, 16) {
9471                                if let Some(ch) = char::from_u32(n) {
9472                                    out.push(ch);
9473                                }
9474                            }
9475                            self.pos += 4;
9476                        }
9477                        _ => {}
9478                    }
9479                    self.pos += 1;
9480                }
9481                // A raw control character is not legal inside a JSON string; it
9482                // has to be escaped. V8 rejects it rather than passing it through.
9483                Some(c) if (c as u32) < 0x20 => {
9484                    return Err(self.err_at("Bad control character in string literal", self.pos))
9485                }
9486                Some(c) => {
9487                    out.push(c);
9488                    self.pos += 1;
9489                }
9490            }
9491        }
9492        Ok(out)
9493    }
9494    fn parse_array(&mut self) -> Result<Value, String> {
9495        self.pos += 1; // [
9496        let mut items = Vec::new();
9497        self.skip_ws();
9498        if self.peek() == Some(']') {
9499            self.pos += 1;
9500            return Ok(with_host(|h| h.new_array(items)));
9501        }
9502        loop {
9503            items.push(self.parse_value()?);
9504            self.skip_ws();
9505            match self.peek() {
9506                Some(',') => {
9507                    self.pos += 1;
9508                }
9509                Some(']') => {
9510                    self.pos += 1;
9511                    break;
9512                }
9513                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9514            }
9515        }
9516        Ok(with_host(|h| h.new_array(items)))
9517    }
9518    fn parse_object(&mut self) -> Result<Value, String> {
9519        self.pos += 1; // {
9520        let mut props: IndexMap<String, Value> = IndexMap::new();
9521        self.skip_ws();
9522        if self.peek() == Some('}') {
9523            self.pos += 1;
9524            return Ok(with_host(|h| h.new_object(props)));
9525        }
9526        loop {
9527            self.skip_ws();
9528            if self.peek() != Some('"') {
9529                // The first key uses the "or '}'" wording (an empty object is
9530                // still legal there); a key after a comma does not. End of input
9531                // reports the same expectation, at the end position.
9532                return Err(if props.is_empty() {
9533                    self.err_at("Expected property name or '}'", self.pos)
9534                } else {
9535                    self.err_at("Expected double-quoted property name", self.pos)
9536                });
9537            }
9538            let key = self.parse_string()?;
9539            self.skip_ws();
9540            if self.peek() != Some(':') {
9541                return Err(match self.peek() {
9542                    None => "SyntaxError: Unexpected end of JSON input".into(),
9543                    _ => self.err_at("Expected ':' after property name", self.pos),
9544                });
9545            }
9546            self.pos += 1;
9547            let val = self.parse_value()?;
9548            props.insert(key, val);
9549            self.skip_ws();
9550            match self.peek() {
9551                Some(',') => {
9552                    self.pos += 1;
9553                }
9554                Some('}') => {
9555                    self.pos += 1;
9556                    break;
9557                }
9558                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9559            }
9560        }
9561        Ok(with_host(|h| h.new_object(props)))
9562    }
9563}
9564
9565// ══ type methods (array / string / number) ═══════════════════════════════════
9566
9567fn is_array_method(name: &str) -> bool {
9568    matches!(
9569        name,
9570        "push"
9571            | "pop"
9572            | "shift"
9573            | "unshift"
9574            | "map"
9575            | "filter"
9576            | "forEach"
9577            | "join"
9578            | "slice"
9579            | "indexOf"
9580            | "lastIndexOf"
9581            | "includes"
9582            | "reduce"
9583            | "concat"
9584            | "reverse"
9585            | "sort"
9586            | "find"
9587            | "findIndex"
9588            | "some"
9589            | "every"
9590            | "flat"
9591            | "fill"
9592            | "splice"
9593            | "keys"
9594            | "values"
9595            | "entries"
9596            | "flatMap"
9597            | "at"
9598            | "toString"
9599            | "reduceRight"
9600            | "findLast"
9601            | "findLastIndex"
9602            | "copyWithin"
9603    )
9604}
9605/// Every `String.prototype` method node-js implements.
9606///
9607/// A LIST rather than a `matches!` arm because the same set has to be installed
9608/// on the real `String.prototype` object: a method read off the prototype
9609/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9610/// found nothing there, so the two views of "which methods exist" would drift
9611/// if they were written twice.
9612pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9613    "toUpperCase",
9614    "toLowerCase",
9615    "charAt",
9616    "charCodeAt",
9617    "codePointAt",
9618    "indexOf",
9619    "lastIndexOf",
9620    "includes",
9621    "slice",
9622    "substring",
9623    "substr",
9624    "split",
9625    "trim",
9626    "trimStart",
9627    "trimEnd",
9628    "replace",
9629    "replaceAll",
9630    "repeat",
9631    "startsWith",
9632    "endsWith",
9633    "padStart",
9634    "padEnd",
9635    "concat",
9636    "at",
9637    "toString",
9638    "toLocaleString",
9639    "valueOf",
9640    "match",
9641    "matchAll",
9642    "search",
9643    "normalize",
9644    "localeCompare",
9645    "toLocaleUpperCase",
9646    "toLocaleLowerCase",
9647    "isWellFormed",
9648    "toWellFormed",
9649];
9650
9651fn is_string_method(name: &str) -> bool {
9652    STRING_PROTO_METHODS.contains(&name)
9653}
9654
9655/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9656/// spelled the way this frontend spells the key (`@@iterator`).
9657///
9658/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9659/// .prototype`) installs its methods from a list, and only the string-keyed
9660/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9661/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9662/// `Builtin` namespace and its table gate, answered a function. Derived from
9663/// the table rather than written out, so it cannot name a method node does not
9664/// define nor miss one it does.
9665pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9666    let prefix = format!("@proto:{ctor}:");
9667    crate::arity::BUILTIN_ARITY
9668        .iter()
9669        .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9670        .filter(|m| m.starts_with("@@"))
9671        .collect()
9672}
9673
9674/// The builtin constructors whose `.prototype` object is BRANDED — every other
9675/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9676///
9677/// Measured on node v26.8.1 over every constructor this frontend knows:
9678///
9679/// ```text
9680/// Array/Object/Number/String/Boolean/Function   the ES5 legacy slot prototypes
9681/// Symbol/BigInt/Map/Set/WeakMap/WeakSet         carry an own @@toStringTag
9682/// Promise/Iterator/ArrayBuffer/DataView         "
9683/// WeakRef/FinalizationRegistry/URL              "
9684/// URLSearchParams/TextEncoder/TextDecoder       "
9685/// Date/RegExp/Error/TypeError/Uint8Array/…      [object Object]
9686/// ```
9687///
9688/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9689/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9690/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9691/// its own failure message where node names `[object Object]`.
9692pub(crate) const BRANDED_PROTOS: &[&str] = &[
9693    "Array",
9694    "ArrayBuffer",
9695    "BigInt",
9696    "Boolean",
9697    "DataView",
9698    "FinalizationRegistry",
9699    "Function",
9700    "Iterator",
9701    "Map",
9702    "Number",
9703    "Object",
9704    "Promise",
9705    "Set",
9706    "SharedArrayBuffer",
9707    "String",
9708    "Symbol",
9709    "TextDecoder",
9710    "TextEncoder",
9711    "URL",
9712    "URLSearchParams",
9713    "WeakMap",
9714    "WeakRef",
9715    "WeakSet",
9716];
9717
9718/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9719/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9720/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9721///
9722/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9723/// than coercing `o` to a pattern — the protocol every regexp-like library
9724/// implements. None of the five were consulted, so a custom matcher was
9725/// silently stringified instead.
9726fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9727    if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9728        return None;
9729    }
9730    let f = get_property(arg, sym).ok()?;
9731    with_host(|h| host::is_callable(h, &f)).then_some(f)
9732}
9733
9734fn is_regexp_arg(v: &Value) -> bool {
9735    // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9736    // itself a regexp — or a real one can disown the label. Only the heap kind
9737    // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9738    // the TypeError the spec requires.
9739    if let Ok(m) = get_property(v, "@@match") {
9740        if !matches!(m, Value::Undef) {
9741            return with_host(|h| h.truthy(&m));
9742        }
9743    }
9744    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9745}
9746
9747/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9748/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9749fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9750    if pat.is_empty() {
9751        return Ok(s.to_string());
9752    }
9753    let mut out = String::new();
9754    let mut rest = s;
9755    let mut base = 0usize;
9756    while let Some(pos) = rest.find(pat) {
9757        out.push_str(&rest[..pos]);
9758        let offset = base + pos;
9759        let m = with_host(|h| h.new_str(pat.to_string()));
9760        let str_arg = with_host(|h| h.new_str(s.to_string()));
9761        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9762        out.push_str(&with_host(|h| h.str_of(&r)));
9763        let consumed = pos + pat.len();
9764        base += consumed;
9765        rest = &rest[consumed..];
9766        if !all {
9767            break;
9768        }
9769    }
9770    out.push_str(rest);
9771    Ok(out)
9772}
9773/// Every `Number.prototype` method node-js implements — a list for the same
9774/// reason [`STRING_PROTO_METHODS`] is one.
9775pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9776    "toFixed",
9777    "toExponential",
9778    "toString",
9779    "toPrecision",
9780    "toLocaleString",
9781    "valueOf",
9782];
9783
9784fn is_number_method(name: &str) -> bool {
9785    NUMBER_PROTO_METHODS.contains(&name)
9786}
9787
9788/// The exotic kinds whose own dispatch table does NOT already reach the
9789/// `Object.prototype` methods, so the inherited ones have to be routed to.
9790///
9791/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9792/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9793/// it to the object form made `(255).toString(16)` report `[object Number]`.
9794fn inherits_object_methods(recv: &Value) -> bool {
9795    matches!(
9796        with_host(|h| h.kind_of(recv)),
9797        Some(
9798            ObjKind::Map
9799                | ObjKind::Set
9800                | ObjKind::Promise
9801                | ObjKind::RegExp
9802                | ObjKind::Generator
9803                | ObjKind::Symbol
9804                | ObjKind::BigInt
9805                | ObjKind::Iter
9806        )
9807    )
9808}
9809
9810/// Whether `recv`'s own prototype defines `name`, shadowing the
9811/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9812/// `Map.prototype` does not.
9813fn overrides_object_method(recv: &Value, name: &str) -> bool {
9814    match with_host(|h| h.kind_of(recv)) {
9815        Some(ObjKind::Map) => is_map_method(name),
9816        Some(ObjKind::Set) => is_set_method(name),
9817        Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9818        // A Symbol has its own `toString`; `valueOf` is the inherited one,
9819        // which returns the receiver — exactly what a symbol needs.
9820        Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9821        Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9822        _ => false,
9823    }
9824}
9825
9826/// Dispatch `recv.name(args)` for the built-in prototype methods.
9827pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9828    // A USER method on the receiver's prototype chain wins over the builtin of
9829    // the same name — that is how a `class X extends Array` method is reached,
9830    // since the dispatch below goes straight to the builtin table and has no
9831    // entry for it.
9832    //
9833    // Deliberately restricted to a user function: the shared `Object.prototype`
9834    // carries real `@proto:Object:*` thunks, so accepting any callable made a
9835    // bare `map.toString()` resolve to the object form instead of the builtin
9836    // one the exotic is supposed to use.
9837    if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9838        if matches!(
9839            with_host(|h| h.kind_of(&f)),
9840            Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9841        ) {
9842            return host::invoke(&f, args, Some(recv.clone()));
9843        }
9844    }
9845    // A method synthesized from the receiver's KIND is unreachable once its
9846    // intrinsic prototype is off the chain. The read already answers
9847    // `undefined` for one; dispatch has its own table and would still have
9848    // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9849    // while `a.join` was `undefined` — the read and the call disagreeing again,
9850    // in the opposite direction from the monkey-patch case below.
9851    if !own_intrinsic_reachable(recv)
9852        && inherited_method_owner(recv, name).is_none()
9853        && !has_own_for_shadow(recv, name)
9854        && inherited_builtin_static(recv, name).is_none()
9855        && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9856    {
9857        return Err(host::type_error(&format!("{name} is not a function")));
9858    }
9859    // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9860    // path resolves these, but dispatch goes straight to the builtin table and
9861    // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9862    // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9863    // about the same name, on the one path a polyfill actually uses.
9864    if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9865        if let Some(f) = inherited_builtin_static(recv, name) {
9866            if with_host(|h| host::is_callable(h, &f)) {
9867                return host::invoke(&f, args, Some(recv.clone()));
9868            }
9869        }
9870    }
9871    // Every object INHERITS the `Object.prototype` methods, and an exotic that
9872    // does not define its own reaches them the same way. Each kind's dispatch
9873    // table below only knows its own methods, so `new Map().toString()`,
9874    // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9875    // not a function" — `Object.prototype.toString.call(m)` worked while
9876    // `m.toString()` did not.
9877    // The allowlist is the kinds whose own dispatch table below would otherwise
9878    // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9879    // the same way — a function, a class and a bound function included, where
9880    // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9881    // resolved it. `inherited_method_owner` decides which prototype owns the
9882    // name, so a kind that defines its own still gets its own.
9883    if is_object_builtin_method(name)
9884        && (inherited_method_owner(recv, name) == Some("Object")
9885            || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9886    {
9887        // `toString` goes through the branded form (20.1.3.6), which reads
9888        // `Symbol.toStringTag` and falls back to the receiver's own brand —
9889        // `[object Map]`, not the generic stringification.
9890        if name == "toString" {
9891            return proto_method(recv, "Object:toString", args);
9892        }
9893        return object_builtin_method(recv, name, args);
9894    }
9895    // `Object.prototype.valueOf` is inherited by every exotic that does not
9896    // override it (an Array does not), and returns the receiver. Without this
9897    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9898    // and threw `valueOf is not a function`.
9899    if name == "valueOf"
9900        && matches!(
9901            with_host(|h| h.kind_of(recv)),
9902            Some(
9903                ObjKind::Array
9904                    | ObjKind::Map
9905                    | ObjKind::Set
9906                    | ObjKind::Generator
9907                    | ObjKind::Promise
9908                    | ObjKind::Iter
9909                    | ObjKind::RegExp
9910            )
9911        )
9912    {
9913        return Ok(recv.clone());
9914    }
9915    // Only the tag is needed to pick the branch — cloning the receiver here made
9916    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9917    match with_host(|h| h.kind_of(recv)) {
9918        Some(ObjKind::Array) => array_method(recv, name, args),
9919        Some(ObjKind::Str) => {
9920            // `string_method` consumes the text itself, so this clone is the
9921            // payload, not a tag probe.
9922            let s = peek(recv, |o| match o {
9923                JsObj::Str(s) => Some(s.clone()),
9924                _ => None,
9925            })
9926            .unwrap_or_default();
9927            string_method(&s, name, args)
9928        }
9929        Some(ObjKind::Map) => map_method(recv, name, args),
9930        Some(ObjKind::Set) => set_method(recv, name, args),
9931        Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9932            crate::stdlib::iterator::call(recv, name, &args)
9933        }
9934        Some(ObjKind::Generator) => generator_method(recv, name, args),
9935        Some(ObjKind::Promise) => promise_method(recv, name, args),
9936        Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9937            crate::stdlib::iterator::call(recv, name, &args)
9938        }
9939        Some(ObjKind::Iter) => iter_method(recv, name, args),
9940        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9941        Some(ObjKind::BigInt) => {
9942            let b = peek(recv, |o| match o {
9943                JsObj::BigInt(b) => Some(b.clone()),
9944                _ => None,
9945            })
9946            .unwrap_or_default();
9947            bigint_method(&b, name, args)
9948        }
9949        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9950        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9951            match function_builtin_method(recv, name, &args)? {
9952                Some(v) => Ok(v),
9953                None => Err(host::type_error(&format!("{name} is not a function"))),
9954            }
9955        }
9956        Some(ObjKind::Object) => {
9957            if let Some(f) = peek(recv, |o| match o {
9958                JsObj::Object(p) => p.get(name).cloned(),
9959                _ => None,
9960            }) {
9961                host::invoke(&f, args, Some(recv.clone()))
9962            } else if name == "hasOwnProperty" {
9963                let k = with_host(|h| h.str_of(&arg0(&args)));
9964                let has = peek(recv, |o| match o {
9965                    JsObj::Object(p) => Some(p.contains_key(&k)),
9966                    _ => None,
9967                })
9968                .unwrap_or(false);
9969                Ok(Value::Bool(has))
9970            } else if name == "toString" {
9971                Ok(with_host(|h| h.new_str("[object Object]")))
9972            } else {
9973                Err(host::type_error(&format!("{} is not a function", name)))
9974            }
9975        }
9976        _ => {
9977            // Primitive number/bool/string coercions.
9978            if let Value::Float(_) | Value::Int(_) = recv {
9979                return number_method(with_host(|h| h.to_number(recv)), name, args);
9980            }
9981            if let Some(s) = with_host(|h| h.as_str(recv)) {
9982                return string_method(&s, name, args);
9983            }
9984            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9985            // so it reached no branch at all and `true.toString()` threw `is not
9986            // a function`. Its three methods are `toString`, `valueOf`, and the
9987            // inherited `Object.prototype.toLocaleString` — which
9988            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9989            // was reachable from the array form too.
9990            if let Value::Bool(b) = recv {
9991                return match name {
9992                    "toString" | "toLocaleString" => {
9993                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
9994                    }
9995                    "valueOf" => Ok(Value::Bool(*b)),
9996                    _ => Err(host::type_error(&format!("{name} is not a function"))),
9997                };
9998            }
9999            Err(host::type_error(&format!("{} is not a function", name)))
10000        }
10001    }
10002}
10003
10004/// A copy of the whole backing store, for the methods that genuinely consume
10005/// every element (`map`, `filter`, `join`, …). Never call it just to read
10006/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10007/// A LIVE iterator over a `Map` or `Set`.
10008///
10009/// Node's collection iterators see the collection as it is at each step: an
10010/// entry added during iteration IS visited, and one deleted before it is
10011/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10012/// a loop that deletes as it goes still processed the entries it had removed.
10013///
10014/// The cursor is the last key yielded plus the index it was at. On each step
10015/// the key is located again in the CURRENT order: if it is still there the next
10016/// entry follows it, and if it was itself deleted the stored index now names
10017/// the entry that shifted into its place. That reproduces node for the cases
10018/// its own tests turn on — add-during, delete-ahead, delete-self,
10019/// delete-behind, delete-the-rest and clear — without giving `Map` the
10020/// tombstoned entry list node uses internally.
10021fn collection_iterator(coll: &Value, kind: &str) -> Value {
10022    with_host(|h| {
10023        let mut m = IndexMap::new();
10024        m.insert(
10025            "@@native".into(),
10026            h.new_str("CollectionIterator".to_string()),
10027        );
10028        m.insert("@@coll".into(), coll.clone());
10029        m.insert("@@kind".into(), h.new_str(kind.to_string()));
10030        m.insert("@@started".into(), Value::Bool(false));
10031        m.insert("@@lastIdx".into(), Value::Float(0.0));
10032        h.new_object(m)
10033    })
10034}
10035
10036/// One step of a live collection iterator.
10037pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10038    let slot = |k: &str| {
10039        with_host(|h| match h.get(recv) {
10040            Some(JsObj::Object(p)) => p.get(k).cloned(),
10041            _ => None,
10042        })
10043    };
10044    let coll = slot("@@coll").unwrap_or(Value::Undef);
10045    let kind = slot("@@kind")
10046        .map(|v| with_host(|h| h.str_of(&v)))
10047        .unwrap_or_default();
10048    let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10049    let last_idx = slot("@@lastIdx")
10050        .map(|v| with_host(|h| h.to_number(&v)) as usize)
10051        .unwrap_or(0);
10052    let last_key = slot("@@lastKey");
10053
10054    let next_idx = if !started {
10055        0
10056    } else {
10057        match last_key
10058            .as_ref()
10059            .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10060        {
10061            // Still present: continue after it.
10062            Some(i) => i + 1,
10063            // Deleted since: whatever shifted into its slot is next.
10064            None => last_idx,
10065        }
10066    };
10067    let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10068    let Some((k, v)) = entry else {
10069        return Ok(iter_result(Value::Undef, true));
10070    };
10071    with_host(|h| {
10072        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10073            p.insert("@@started".into(), Value::Bool(true));
10074            p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10075            p.insert("@@lastKey".into(), k.clone());
10076        }
10077    });
10078    let out = match kind.as_str() {
10079        "keys" => k,
10080        "values" => v,
10081        _ => with_host(|h| h.new_array(vec![k, v])),
10082    };
10083    Ok(iter_result(out, false))
10084}
10085
10086/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10087fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10088    match h.get(coll) {
10089        Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10090        Some(JsObj::Set { entries, .. }) => {
10091            entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10092        }
10093        _ => None,
10094    }
10095}
10096
10097/// Where `key` currently sits in the collection's order.
10098fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10099    let mk = host::map_key(h, key);
10100    match h.get(coll) {
10101        Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10102        Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10103        _ => None,
10104    }
10105}
10106
10107/// The `thisArg` an iteration method was given, if any.
10108///
10109/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10110/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10111/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10112/// `Array.from`'s map function. Every one of them was invoking the callback
10113/// with no receiver, so `this` inside it was undefined and the argument did
10114/// nothing.
10115fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10116    args.get(idx)
10117        .filter(|v| !matches!(v, Value::Undef))
10118        .cloned()
10119}
10120
10121/// The elements of an array, with any INDEX ACCESSOR resolved.
10122///
10123/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10124/// table, and an array's elements live in a backing vector — so every method
10125/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10126/// slot and never called the getter, while a plain `arr[1]` read did.
10127///
10128/// An array with no accessors pays one lookup returning an empty list, so the
10129/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10130/// since calling one re-enters.
10131/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10132/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10133/// read LIVE at its index, and an index that no longer exists is skipped.
10134///
10135/// Snapshotting the whole array instead meant a callback that mutated it was
10136/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10137/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10138///
10139/// `f` returns `Some(x)` to stop early with `x`.
10140fn array_walk<T>(
10141    recv: &Value,
10142    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10143) -> Result<Option<T>, String> {
10144    let len = array_len(recv);
10145    for i in 0..len {
10146        // A HOLE — and an index a shrinking mutation has dropped — is skipped
10147        // without calling the callback.
10148        if index_absent(recv, i) || i >= array_len(recv) {
10149            continue;
10150        }
10151        let v = get_property(recv, &i.to_string())?;
10152        if let Some(out) = f(i, v)? {
10153            return Ok(Some(out));
10154        }
10155    }
10156    Ok(None)
10157}
10158
10159/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10160/// capture-length-once, read-each-element-live rule walked from the end. A
10161/// callback that SHRINKS the array is observed by every later step, so the
10162/// indices it drops are skipped rather than served from a stale copy.
10163fn array_walk_rev<T>(
10164    recv: &Value,
10165    from: usize,
10166    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10167) -> Result<Option<T>, String> {
10168    for i in (0..from).rev() {
10169        if index_absent(recv, i) || i >= array_len(recv) {
10170            continue;
10171        }
10172        let v = get_property(recv, &i.to_string())?;
10173        if let Some(out) = f(i, v)? {
10174            return Ok(Some(out));
10175        }
10176    }
10177    Ok(None)
10178}
10179
10180/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10181/// `undefined` once a mutation has shrunk the array past it.
10182fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10183    if i >= array_len(recv) {
10184        return Ok(Value::Undef);
10185    }
10186    get_property(recv, &i.to_string())
10187}
10188
10189fn array_items(recv: &Value) -> Vec<Value> {
10190    let mut items = with_host(|h| match h.get(recv) {
10191        Some(JsObj::Array(items)) => items.clone(),
10192        _ => Vec::new(),
10193    });
10194    resolve_index_accessors(recv, &mut items);
10195    items
10196}
10197
10198/// Replace each slot that has an own accessor with what its getter returns.
10199pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10200    resolve_index_accessors(recv, items);
10201}
10202
10203/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10204/// the ORIGINAL array when nothing changed, and the original still holds the
10205/// stale slots.
10206fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10207    let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10208        .into_iter()
10209        .filter_map(|k| k.parse::<usize>().ok())
10210        .filter(|i| *i < items.len())
10211        .collect();
10212    // An ELIDED index the prototype chain supplies is stale in the backing
10213    // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10214    // value. Spread and `JSON.stringify` both read through here, and both
10215    // rendered the hole rather than what `a[i]` reads.
10216    let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10217        .into_iter()
10218        .filter(|i| *i < items.len() && !indices.contains(i))
10219        .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10220        .collect();
10221    indices.extend(inherited);
10222    let mut replaced = false;
10223    for i in indices {
10224        if let Ok(v) = get_property(recv, &i.to_string()) {
10225            items[i] = v;
10226            replaced = true;
10227        }
10228    }
10229    replaced
10230}
10231
10232/// The ELIDED positions of array `recv` as a membership set. A dense array —
10233/// which is nearly every array — answers with an empty set after a single
10234/// negative hash probe and allocates nothing.
10235///
10236/// The iteration methods split into two groups, and the split is not a matter of
10237/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10238/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10239/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10240/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10241fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10242    with_host(|h| h.hole_indices(recv)).into_iter().collect()
10243}
10244
10245/// The indices `recv` genuinely has NO property at — the elided ones the
10246/// prototype chain does not supply either.
10247///
10248/// Every array method tests `HasProperty` before deciding to skip a position
10249/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10250/// alone made an inherited element invisible to all of them: with
10251/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10252/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10253/// the same position.
10254///
10255/// `hole_set` remains the elision record itself, which is what `splice` moves
10256/// around — that bookkeeping is about the array's OWN storage and must not
10257/// consult the chain.
10258fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10259    hole_set(recv)
10260        .into_iter()
10261        .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10262        .collect()
10263}
10264
10265/// The single-index form of [`absent_set`], for the walkers that test one
10266/// position at a time.
10267fn index_absent(recv: &Value, i: usize) -> bool {
10268    with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10269}
10270
10271/// The element count, without copying the elements.
10272fn array_len(recv: &Value) -> usize {
10273    peek(recv, |o| match o {
10274        JsObj::Array(items) => Some(items.len()),
10275        _ => None,
10276    })
10277    .unwrap_or(0)
10278}
10279
10280/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10281/// array method builds its RESULT with.
10282///
10283/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10284/// produce an array of the receiver's own species, so on a `class A extends
10285/// Array` the result is an `A`. Every one of them allocated a plain array
10286/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10287///
10288/// The default `get [Symbol.species]() { return this }` is what makes the
10289/// subclass the species; a class overriding it with `Array` gets a plain array
10290/// back, which is the documented way to opt out.
10291/// Build an array-shaped result through `ctor`, or a plain array when there is
10292/// none to build through.
10293///
10294/// The constructor is called with the LENGTH and the elements written after, as
10295/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10296/// constructor observe the allocation.
10297fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10298    let Some(ctor) = ctor.filter(|c| {
10299        matches!(
10300            with_host(|h| h.kind_of(c)),
10301            Some(ObjKind::Class) | Some(ObjKind::Func)
10302        )
10303    }) else {
10304        return Ok(with_host(|h| h.new_array(items)));
10305    };
10306    let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10307    write_elements(&out, items);
10308    Ok(out)
10309}
10310
10311/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10312/// hole marks the length-only construction left behind.
10313///
10314/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10315/// the elements written over them stayed marked — so every subclass result of
10316/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10317/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10318fn write_elements(out: &Value, items: Vec<Value>) {
10319    with_host(|h| {
10320        h.clear_holes(out);
10321        if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10322            *dst = items;
10323        }
10324    });
10325}
10326
10327fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10328    let plain = || with_host(|h| h.new_array(items.clone()));
10329    // Only a subclass instance can have a species of its own: a plain array's
10330    // `constructor` is the `Array` builtin, whose species is `Array`.
10331    // A chain lookup, not `get_property`: an Array receiver resolves its
10332    // properties through the stdlib funnel, which has no `constructor` entry,
10333    // so the read alone reports `undefined` for every subclass instance. A
10334    // Proxy is the exception — it has no property map to walk, and its
10335    // `constructor` comes from the `get` trap, so a proxied subclass array
10336    // produced plain arrays.
10337    let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10338        get_property(recv, "constructor").unwrap_or(Value::Undef)
10339    } else {
10340        with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10341    };
10342    if !matches!(
10343        with_host(|h| h.kind_of(&ctor)),
10344        Some(ObjKind::Class) | Some(ObjKind::Func)
10345    ) {
10346        return Ok(plain());
10347    }
10348    // An explicit `@@species` wins; absent one, the constructor itself is the
10349    // species, as the inherited accessor returns `this`.
10350    let species = match get_property(&ctor, "@@species") {
10351        Ok(Value::Undef) => ctor,
10352        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10353        Ok(s) => s,
10354        Err(_) => ctor,
10355    };
10356    if !matches!(
10357        with_host(|h| h.kind_of(&species)),
10358        Some(ObjKind::Class) | Some(ObjKind::Func)
10359    ) {
10360        return Ok(plain());
10361    }
10362    let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10363    // The constructor is called with the LENGTH, so the elements are written
10364    // afterwards — which is also what lets a subclass constructor observe the
10365    // allocation, as node's does.
10366    write_elements(&out, items);
10367    Ok(out)
10368}
10369
10370fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10371    array_method_on(recv, recv, name, args)
10372}
10373
10374/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10375/// generic path to copy the result back onto the array-like.
10376const ARRAY_MUTATORS: &[&str] = &[
10377    "push",
10378    "pop",
10379    "shift",
10380    "unshift",
10381    "splice",
10382    "sort",
10383    "reverse",
10384    "fill",
10385    "copyWithin",
10386];
10387
10388/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10389/// a DOM-ish collection, `arguments`).
10390///
10391/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10392/// rather than over an Array's element vector, so the receiver only has to have
10393/// a `length`. The elements are read out into a temporary Array, the ordinary
10394/// implementation runs on that, and a MUTATING method writes the result back —
10395/// which keeps one implementation of each method rather than a second, generic
10396/// one that could drift from it.
10397///
10398/// An index the receiver does not own is a HOLE in the temporary, so the
10399/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10400fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10401    let len = match get_property(recv, "length") {
10402        Ok(v) => host::to_array_length(&v).unwrap_or(0),
10403        Err(_) => 0,
10404    };
10405    // A STRING receiver owns every index of its length; `has_property` answers
10406    // for objects and reports none of them, which made `[].map.call('abc', f)`
10407    // an array of three holes.
10408    let dense = with_host(|h| h.as_str(recv)).is_some();
10409    let mut items = Vec::with_capacity(len);
10410    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10411    for i in 0..len {
10412        let k = i.to_string();
10413        if dense || has_property(recv, &k)? {
10414            items.push(get_property(recv, &k)?);
10415        } else {
10416            holes.insert(i);
10417            items.push(Value::Undef);
10418        }
10419    }
10420    let tmp = with_host(|h| {
10421        let a = h.new_array(items);
10422        h.install_holes(&a, holes);
10423        a
10424    });
10425    let out = array_method_on(&tmp, recv, method, args)?;
10426    if ARRAY_MUTATORS.contains(&method) {
10427        let result = with_host(|h| match h.get(&tmp) {
10428            Some(JsObj::Array(items)) => items.clone(),
10429            _ => Vec::new(),
10430        });
10431        for (i, v) in result.iter().enumerate() {
10432            set_property(recv, &i.to_string(), v.clone())?;
10433        }
10434        set_property(recv, "length", Value::Float(result.len() as f64))?;
10435    }
10436    Ok(out)
10437}
10438
10439/// `Array.prototype.<name>` on `recv`.
10440///
10441/// `this_value` is what a callback receives as its third argument and what a
10442/// mutating method returns — the same object as `recv` for an ordinary array
10443/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10444/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10445fn array_method_on(
10446    recv: &Value,
10447    this_value: &Value,
10448    name: &str,
10449    args: Vec<Value>,
10450) -> Result<Value, String> {
10451    let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10452    match name {
10453        "push" => {
10454            // 23.1.3.23 step 4 defines each new element through
10455            // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10456            // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10457            // TypeError. The elements were appended to the backing vector
10458            // regardless, so sealing an array did not seal it.
10459            if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10460                let at = array_len(recv);
10461                return Err(host::type_error(&format!(
10462                    "Cannot add property {at}, object is not extensible"
10463                )));
10464            }
10465            // Step 5 then SETS `length`, so a non-writable one refuses the push
10466            // too — `defineProperty(a, 'length', {writable: false})` makes an
10467            // array append-proof without sealing it.
10468            if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10469                return Err(host::type_error(
10470                    "Cannot assign to read only property 'length' of object '[object Array]'",
10471                ));
10472            }
10473            // `push` returns the new length; take it from the same mutable
10474            // borrow rather than copying the array back out to count it.
10475            let len = with_host(|h| {
10476                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10477                    items.extend(args.iter().cloned());
10478                    items.len()
10479                } else {
10480                    0
10481                }
10482            });
10483            Ok(Value::Float(len as f64))
10484        }
10485        "pop" => Ok(with_host(|h| {
10486            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10487                items.pop().unwrap_or(Value::Undef)
10488            } else {
10489                Value::Undef
10490            };
10491            let len = match h.get(recv) {
10492                Some(JsObj::Array(items)) => items.len(),
10493                _ => 0,
10494            };
10495            h.truncate_holes(recv, len);
10496            popped
10497        })),
10498        "shift" => Ok(with_host(|h| {
10499            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10500                if items.is_empty() {
10501                    Value::Undef
10502                } else {
10503                    items.remove(0)
10504                }
10505            } else {
10506                Value::Undef
10507            };
10508            h.remap_holes(recv, |i| i.checked_sub(1));
10509            shifted
10510        })),
10511        "unshift" => {
10512            with_host(|h| {
10513                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10514                    for (i, a) in args.iter().enumerate() {
10515                        items.insert(i, a.clone());
10516                    }
10517                }
10518                let n = args.len();
10519                h.remap_holes(recv, |i| Some(i + n));
10520            });
10521            Ok(Value::Float(array_len(recv) as f64))
10522        }
10523        "join" => {
10524            let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10525                ",".to_string()
10526            } else {
10527                arg_to_string(&args, 0)?
10528            };
10529            join_array(recv, &sep)
10530        }
10531        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10532        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10533        // empty string. It threw `is not a function` — the whole method was
10534        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10535        "toLocaleString" => {
10536            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10537            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10538            if !host::join_stack_push(recv) {
10539                return Ok(with_host(|h| h.new_str(String::new())));
10540            }
10541            let items = array_items(recv);
10542            let mut parts: Vec<String> = Vec::with_capacity(items.len());
10543            for it in &items {
10544                if with_host(|h| h.is_nullish(it)) {
10545                    parts.push(String::new());
10546                    continue;
10547                }
10548                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10549                    Ok(v) => v,
10550                    Err(e) => {
10551                        host::join_stack_pop();
10552                        return Err(e);
10553                    }
10554                };
10555                parts.push(with_host(|h| h.str_of(&v)));
10556            }
10557            host::join_stack_pop();
10558            Ok(with_host(|h| h.new_str(parts.join(","))))
10559        }
10560        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10561        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10562        // `Get`-based `includes` reports `true` for the same array.
10563        "indexOf" => {
10564            let target = arg0(&args);
10565            let len = array_len(recv);
10566            let start = search_start(arg_num(&args, 1), len);
10567            let mut idx = None;
10568            for i in start..len {
10569                // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10570                // index a mutation has since dropped — is skipped, not compared.
10571                if index_absent(recv, i) || i >= array_len(recv) {
10572                    continue;
10573                }
10574                let x = get_property(recv, &i.to_string())?;
10575                if with_host(|h| h.strict_eq(&x, &target)) {
10576                    idx = Some(i);
10577                    break;
10578                }
10579            }
10580            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10581        }
10582        "lastIndexOf" => {
10583            let items = array_items(recv);
10584            let holes = absent_set(recv);
10585            let target = arg0(&args);
10586            let from = (args.len() > 1).then(|| arg_num(&args, 1));
10587            let idx = match search_start_last(from, items.len()) {
10588                None => None,
10589                Some(start) => with_host(|h| {
10590                    items[..=start]
10591                        .iter()
10592                        .enumerate()
10593                        .rev()
10594                        .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10595                        .map(|(i, _)| i)
10596                }),
10597            };
10598            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10599        }
10600        "includes" => {
10601            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10602            // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10603            // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10604            let target = arg0(&args);
10605            let tnan = matches!(target, Value::Float(f) if f.is_nan());
10606            let len = array_len(recv);
10607            let start = search_start(arg_num(&args, 1), len);
10608            let mut found = false;
10609            for i in start..len {
10610                let x = array_elem_live(recv, i)?;
10611                if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10612                    || with_host(|h| h.strict_eq(&x, &target))
10613                {
10614                    found = true;
10615                    break;
10616                }
10617            }
10618            Ok(Value::Bool(found))
10619        }
10620        "slice" => {
10621            let items = array_items(recv);
10622            let (lo, hi) = slice_bounds(&args, items.len());
10623            let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10624            with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10625            Ok(out)
10626        }
10627        "concat" => {
10628            // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10629            // is spread, overriding `IsArray` in BOTH directions: a plain
10630            // array-like opts IN, and an array opts OUT.
10631            let spreadable = |a: &Value| -> bool {
10632                let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10633                if matches!(flag, Value::Undef) {
10634                    matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10635                        && !is_arguments(a)
10636                } else {
10637                    with_host(|h| h.truthy(&flag))
10638                }
10639            };
10640            // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10641            // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10642            // element, its `ToObject` box, not the characters `array_generic`
10643            // read out of it. A hole in a spread receiver or argument stays a
10644            // hole in the result, at its shifted position.
10645            let (mut out, mut holes) = if spreadable(this_value) {
10646                (array_items(recv), absent_set(recv))
10647            } else {
10648                (vec![to_object(this_value)], Default::default())
10649            };
10650            let mut sources: Vec<(Value, usize)> = Vec::new();
10651            for a in &args {
10652                if !spreadable(a) {
10653                    out.push(a.clone());
10654                    continue;
10655                }
10656                match with_host(|h| h.get(a).cloned()) {
10657                    // Read off the backing vector rather than through
10658                    // `array_items`, so the resolve that does for the receiver
10659                    // has to be done here too: 23.1.3.1 step 5.c.iv is a
10660                    // `[[Get]]`, and an index with a getter — or an elided one
10661                    // the chain supplies — is stale in that vector.
10662                    Some(JsObj::Array(mut items)) => {
10663                        resolve_index_accessors(a, &mut items);
10664                        sources.push((a.clone(), out.len()));
10665                        out.extend(items);
10666                    }
10667                    // An opted-in array-LIKE spreads by its `length` and index
10668                    // properties rather than by a backing vector it has none of.
10669                    _ => {
10670                        let len = get_property(a, "length").unwrap_or(Value::Undef);
10671                        let n = with_host(|h| h.to_number(&len));
10672                        let n = if n.is_finite() {
10673                            n.max(0.0) as usize
10674                        } else {
10675                            0
10676                        };
10677                        for i in 0..n {
10678                            out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10679                        }
10680                    }
10681                }
10682            }
10683
10684            for (src, base) in sources {
10685                holes.extend(
10686                    with_host(|h| h.hole_indices(&src))
10687                        .into_iter()
10688                        .map(|i| i + base),
10689                );
10690            }
10691            let arr = array_species_create(this_value, out)?;
10692            with_host(|h| h.install_holes(&arr, holes));
10693            Ok(arr)
10694        }
10695        "reverse" => {
10696            let len = array_len(recv);
10697            with_host(|h| {
10698                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10699                    items.reverse();
10700                }
10701                h.remap_holes(recv, |i| Some(len - 1 - i));
10702            });
10703            Ok(this_value.clone())
10704        }
10705        "fill" => {
10706            // fill(value[, start[, end]]) — negative indices count from the end.
10707            let val = arg0(&args);
10708            let len = array_len(recv) as i64;
10709            let norm =
10710                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10711            let start = if args.len() >= 2 {
10712                norm(arg_num(&args, 1) as i64)
10713            } else {
10714                0
10715            };
10716            let end = if args.len() >= 3 {
10717                norm(arg_num(&args, 2) as i64)
10718            } else {
10719                len as usize
10720            };
10721            with_host(|h| {
10722                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10723                    for it in items.iter_mut().take(end).skip(start) {
10724                        *it = val.clone();
10725                    }
10726                }
10727                // Every filled position now holds a real value.
10728                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10729            });
10730            Ok(this_value.clone())
10731        }
10732        "copyWithin" => {
10733            // copyWithin(target, start[, end]) — copy a slice within the array.
10734            let items = array_items(recv);
10735            let len = items.len() as i64;
10736            let norm =
10737                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10738            let target = norm(arg_num(&args, 0) as i64);
10739            let start = if args.len() >= 2 {
10740                norm(arg_num(&args, 1) as i64)
10741            } else {
10742                0
10743            };
10744            let end = if args.len() >= 3 {
10745                norm(arg_num(&args, 2) as i64)
10746            } else {
10747                len as usize
10748            };
10749            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10750            let copied = slice.len();
10751            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10752            // deletes the target when the source has no such property);
10753            // everything outside the written range keeps its own.
10754            let src_holes = absent_set(recv);
10755            with_host(|h| {
10756                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10757                    for (k, v) in slice.into_iter().enumerate() {
10758                        if target + k < a.len() {
10759                            a[target + k] = v;
10760                        }
10761                    }
10762                }
10763                let len = len as usize;
10764                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10765                    .iter()
10766                    .copied()
10767                    .filter(|i| *i < target || *i >= (target + copied).min(len))
10768                    .collect();
10769                for k in 0..copied {
10770                    if target + k < len && src_holes.contains(&(start + k)) {
10771                        holes.insert(target + k);
10772                    }
10773                }
10774                h.install_holes(recv, holes);
10775            });
10776            Ok(this_value.clone())
10777        }
10778        "at" => {
10779            let items = array_items(recv);
10780            let mut i = arg_num(&args, 0) as i64;
10781            if i < 0 {
10782                i += items.len() as i64;
10783            }
10784            Ok(if i >= 0 && (i as usize) < items.len() {
10785                items[i as usize].clone()
10786            } else {
10787                Value::Undef
10788            })
10789        }
10790        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10791        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10792        // twice and yields `[2, <1 empty item>, 6]`.
10793        "map" => {
10794            let holes = absent_set(recv);
10795            let cb = arg0(&args);
10796            // The result keeps the source's LENGTH, so a skipped index still
10797            // occupies a slot; `array_walk` only tells us which ones ran.
10798            let mut out = vec![Value::Undef; array_len(recv)];
10799            array_walk(recv, |i, it| {
10800                let v = host::invoke(
10801                    &cb,
10802                    vec![it, Value::Float(i as f64), this_value.clone()],
10803                    this_arg(&args, 1),
10804                )?;
10805                if i < out.len() {
10806                    out[i] = v;
10807                }
10808                Ok(None::<()>)
10809            })?;
10810            let arr = array_species_create(this_value, out)?;
10811            with_host(|h| h.install_holes(&arr, holes));
10812            Ok(arr)
10813        }
10814        "flatMap" => {
10815            let cb = arg0(&args);
10816            let thisarg = this_arg(&args, 1);
10817            let mut out = Vec::new();
10818            array_walk(recv, |i, v| {
10819                let r = host::invoke(
10820                    &cb,
10821                    vec![v, Value::Float(i as f64), this_value.clone()],
10822                    thisarg.clone(),
10823                )?;
10824                match with_host(|h| h.get(&r).cloned()) {
10825                    Some(JsObj::Array(inner)) => out.extend(inner),
10826                    _ => out.push(r),
10827                }
10828                Ok(None::<()>)
10829            })?;
10830            array_species_create(this_value, out)
10831        }
10832        "filter" => {
10833            let cb = arg0(&args);
10834            let mut out = Vec::new();
10835            array_walk(recv, |i, it| {
10836                let keep = host::invoke(
10837                    &cb,
10838                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10839                    this_arg(&args, 1),
10840                )?;
10841                if with_host(|h| h.truthy(&keep)) {
10842                    out.push(it);
10843                }
10844                Ok(None::<()>)
10845            })?;
10846            array_species_create(this_value, out)
10847        }
10848        "forEach" => {
10849            let cb = arg0(&args);
10850            array_walk(recv, |i, it| {
10851                host::invoke(
10852                    &cb,
10853                    vec![it, Value::Float(i as f64), this_value.clone()],
10854                    this_arg(&args, 1),
10855                )?;
10856                Ok(None::<()>)
10857            })?;
10858            Ok(Value::Undef)
10859        }
10860        "find" => {
10861            let items = array_items(recv);
10862            let cb = arg0(&args);
10863            for (i, it) in items.iter().enumerate() {
10864                let m = host::invoke(
10865                    &cb,
10866                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10867                    this_arg(&args, 1),
10868                )?;
10869                if with_host(|h| h.truthy(&m)) {
10870                    return Ok(it.clone());
10871                }
10872            }
10873            Ok(Value::Undef)
10874        }
10875        "findIndex" => {
10876            let items = array_items(recv);
10877            let cb = arg0(&args);
10878            for (i, it) in items.iter().enumerate() {
10879                let m = host::invoke(
10880                    &cb,
10881                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10882                    this_arg(&args, 1),
10883                )?;
10884                if with_host(|h| h.truthy(&m)) {
10885                    return Ok(Value::Float(i as f64));
10886                }
10887            }
10888            Ok(Value::Float(-1.0))
10889        }
10890        "some" => {
10891            let cb = arg0(&args);
10892            let thisarg = this_arg(&args, 1);
10893            let hit = array_walk(recv, |i, v| {
10894                let m = host::invoke(
10895                    &cb,
10896                    vec![v, Value::Float(i as f64), this_value.clone()],
10897                    thisarg.clone(),
10898                )?;
10899                Ok(with_host(|h| h.truthy(&m)).then_some(()))
10900            })?;
10901            Ok(Value::Bool(hit.is_some()))
10902        }
10903        "every" => {
10904            let cb = arg0(&args);
10905            let failed = array_walk(recv, |i, it| {
10906                let m = host::invoke(
10907                    &cb,
10908                    vec![it, Value::Float(i as f64), this_value.clone()],
10909                    this_arg(&args, 1),
10910                )?;
10911                Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10912            })?;
10913            Ok(Value::Bool(failed.is_none()))
10914        }
10915        "reduce" => {
10916            let items = array_items(recv);
10917            let holes = absent_set(recv);
10918            let cb = arg0(&args);
10919            let acc;
10920            let mut start = 0;
10921            if args.len() >= 2 {
10922                acc = args[1].clone();
10923            } else {
10924                // With no seed the accumulator is the first PRESENT element, so a
10925                // leading run of holes is skipped rather than seeding `undefined`.
10926                match (0..items.len()).find(|i| !holes.contains(i)) {
10927                    Some(i) => {
10928                        acc = items[i].clone();
10929                        start = i + 1;
10930                    }
10931                    None => {
10932                        return Err(host::type_error(
10933                            "Reduce of empty array with no initial value",
10934                        ))
10935                    }
10936                }
10937            }
10938            // Each element is read LIVE at its index, so a callback that
10939            // shrinks the array is observed — the tail is skipped rather than
10940            // folded from a stale snapshot.
10941            let mut cur = acc;
10942            array_walk(recv, |i, it| {
10943                if i < start {
10944                    return Ok(None::<()>);
10945                }
10946                cur = host::invoke(
10947                    &cb,
10948                    vec![
10949                        std::mem::replace(&mut cur, Value::Undef),
10950                        it,
10951                        Value::Float(i as f64),
10952                        this_value.clone(),
10953                    ],
10954                    this_arg(&args, 1),
10955                )?;
10956                Ok(None::<()>)
10957            })?;
10958            Ok(cur)
10959        }
10960        "reduceRight" => {
10961            let cb = arg0(&args);
10962            let n = array_len(recv);
10963            let mut acc;
10964            let mut from = n; // one past the next index to process (walking down)
10965            if args.len() >= 2 {
10966                acc = args[1].clone();
10967            } else {
10968                let holes = absent_set(recv);
10969                match (0..n).rev().find(|i| !holes.contains(i)) {
10970                    Some(k) => {
10971                        acc = get_property(recv, &k.to_string())?;
10972                        from = k;
10973                    }
10974                    None => {
10975                        return Err(host::type_error(
10976                            "Reduce of empty array with no initial value",
10977                        ))
10978                    }
10979                }
10980            }
10981            // `acc` moves into the closure and back out on every step, so it
10982            // lives in an Option the closure can take from and refill.
10983            let mut slot = Some(acc);
10984            array_walk_rev(recv, from, |i, v| {
10985                let prev = slot.take().expect("accumulator is refilled each step");
10986                slot = Some(host::invoke(
10987                    &cb,
10988                    vec![prev, v, Value::Float(i as f64), this_value.clone()],
10989                    None,
10990                )?);
10991                Ok(None::<()>)
10992            })?;
10993            acc = slot.expect("accumulator is refilled each step");
10994            Ok(acc)
10995        }
10996        "findLast" => {
10997            let items = array_items(recv);
10998            let cb = arg0(&args);
10999            for i in (0..items.len()).rev() {
11000                let m = host::invoke(
11001                    &cb,
11002                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11003                    this_arg(&args, 1),
11004                )?;
11005                if with_host(|h| h.truthy(&m)) {
11006                    return Ok(items[i].clone());
11007                }
11008            }
11009            Ok(Value::Undef)
11010        }
11011        "findLastIndex" => {
11012            let items = array_items(recv);
11013            let cb = arg0(&args);
11014            for i in (0..items.len()).rev() {
11015                let m = host::invoke(
11016                    &cb,
11017                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11018                    this_arg(&args, 1),
11019                )?;
11020                if with_host(|h| h.truthy(&m)) {
11021                    return Ok(Value::Float(i as f64));
11022                }
11023            }
11024            Ok(Value::Float(-1.0))
11025        }
11026        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11027        // and the holes are re-created at the tail — `[3,,1].sort()` is
11028        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11029        "sort" => {
11030            let all = array_items(recv);
11031            let holes = absent_set(recv);
11032            let mut items: Vec<Value> = all
11033                .iter()
11034                .enumerate()
11035                .filter(|(i, _)| !holes.contains(i))
11036                .map(|(_, v)| v.clone())
11037                .collect();
11038            sort_values(&mut items, args.first())?;
11039            let present = items.len();
11040            // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11041            // captured at step 1: `Set` for each sorted element, then `Delete`
11042            // for the holes that followed them. Replacing the whole backing
11043            // vector instead discarded anything the COMPARATOR appended —
11044            // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11045            // original length with every pushed element gone.
11046            with_host(|h| {
11047                let len = all.len();
11048                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11049                    if a.len() < len {
11050                        a.resize(len, Value::Undef);
11051                    }
11052                    for (i, v) in items.into_iter().enumerate() {
11053                        a[i] = v;
11054                    }
11055                    for slot in a[present..len].iter_mut() {
11056                        *slot = Value::Undef;
11057                    }
11058                }
11059                h.install_holes(recv, (present..len).collect());
11060            });
11061            Ok(this_value.clone())
11062        }
11063        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11064        "toSorted" => {
11065            let mut items = array_items(recv);
11066            sort_values(&mut items, args.first())?;
11067            Ok(with_host(|h| h.new_array(items)))
11068        }
11069        "toReversed" => {
11070            let mut items = array_items(recv);
11071            items.reverse();
11072            Ok(with_host(|h| h.new_array(items)))
11073        }
11074        "toSpliced" => {
11075            let mut items = array_items(recv);
11076            let len = items.len();
11077            let start = {
11078                let s = arg_num(&args, 0);
11079                if s < 0.0 {
11080                    ((len as f64 + s).max(0.0)) as usize
11081                } else {
11082                    (s as usize).min(len)
11083                }
11084            };
11085            let delete = if args.len() >= 2 {
11086                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11087            } else if args.is_empty() {
11088                0
11089            } else {
11090                len - start
11091            };
11092            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11093            items.splice(start..start + delete, inserts);
11094            Ok(with_host(|h| h.new_array(items)))
11095        }
11096        "with" => {
11097            let mut items = array_items(recv);
11098            let len = items.len() as i64;
11099            let rel = arg_num(&args, 0) as i64;
11100            let idx = if rel < 0 { len + rel } else { rel };
11101            if idx < 0 || idx >= len {
11102                return Err(host::range_error(&format!("Invalid index : {rel}")));
11103            }
11104            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11105            Ok(with_host(|h| h.new_array(items)))
11106        }
11107        "flat" => {
11108            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11109            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11110            let raw = if args.is_empty() {
11111                1.0
11112            } else {
11113                arg_num(&args, 0)
11114            };
11115            let depth = if raw.is_nan() {
11116                0.0
11117            } else if raw.is_infinite() {
11118                raw
11119            } else {
11120                raw.trunc()
11121            };
11122            let mut out = Vec::new();
11123            flatten_into(recv, depth, &mut out)?;
11124            array_species_create(this_value, out)
11125        }
11126        "keys" => {
11127            let n = array_len(recv);
11128            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
11129            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11130        }
11131        "values" | "@@iterator" => {
11132            let items = array_items(recv);
11133            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11134        }
11135        "entries" => {
11136            let items = array_items(recv);
11137            let pairs: Vec<Value> = items
11138                .into_iter()
11139                .enumerate()
11140                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
11141                .collect();
11142            Ok(with_host(|h| {
11143                h.alloc(JsObj::Iter {
11144                    items: pairs,
11145                    idx: 0,
11146                })
11147            }))
11148        }
11149        "splice" => array_splice(recv, args),
11150        // `Array.prototype.toString` IS `join()` with the default separator
11151        // (23.1.3.36), so it converts each element with `ToString` too — and
11152        // shares its cycle cut, which is the whole reason it must not call
11153        // `join_parts` directly: `ToString` of a nested array lands back here.
11154        "toString" => join_array(recv, ","),
11155        // An Array inherits from `Object.prototype` too, so the methods it does
11156        // not override resolve there. `[].hasOwnProperty` already read back as a
11157        // function through the property path, but CALLING it landed here and
11158        // threw `is not a function`.
11159        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11160        _ => Err(host::type_error(&format!("{name} is not a function"))),
11161    }
11162}
11163
11164/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11165/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11166/// cut, which is not optional here: `ToString` of an element that is itself an
11167/// array re-enters through `toString`, so guarding only `join` left
11168/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11169/// process. On node v26.7.0 that expression is `""`.
11170fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11171    if !host::join_stack_push(recv) {
11172        return Ok(with_host(|h| h.new_str(String::new())));
11173    }
11174    // 23.1.3.18 step 6: the length is captured once, then each element is read
11175    // and STRINGIFIED before the next is read. Both halves are observable —
11176    // a getter or a `toString` that shrinks the array is seen by every later
11177    // element, which a read-all-then-convert pass misses.
11178    let parts = (|| -> Result<Vec<String>, String> {
11179        let len = array_len(recv);
11180        let mut out = Vec::with_capacity(len);
11181        for i in 0..len {
11182            let v = array_elem_live(recv, i)?;
11183            out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11184        }
11185        Ok(out)
11186    })();
11187    host::join_stack_pop();
11188    let s = parts?.join(sep);
11189    Ok(with_host(|h| h.new_str(s)))
11190}
11191
11192/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11193/// `null`/`undefined` element contributes the empty string, every other element
11194/// is `ToString(element)` — which for an object means invoking its `toString`,
11195/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11196/// `"[object Object]"`.
11197///
11198/// The all-primitive array — the overwhelmingly common one — is rendered under
11199/// a single host borrow; only an array actually holding an object pays for the
11200/// re-entrant per-element conversion.
11201fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11202    let fast = with_host(|h| {
11203        items
11204            .iter()
11205            .map(|x| match x {
11206                Value::Undef => Some(String::new()),
11207                _ if h.is_null(x) => Some(String::new()),
11208                // A SYMBOL element is primitive but has no `ToString`, so it must
11209                // fall through to the fallible path and throw there:
11210                // `[Symbol()].join()` is a TypeError on node v26.7.0.
11211                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11212                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11213                _ => None,
11214            })
11215            .collect::<Vec<_>>()
11216    });
11217    if fast.iter().all(Option::is_some) {
11218        return Ok(fast.into_iter().flatten().collect());
11219    }
11220    let mut out = Vec::with_capacity(items.len());
11221    for (x, p) in items.iter().zip(fast) {
11222        match p {
11223            Some(s) => out.push(s),
11224            None => {
11225                let s = host::to_string_value(x)?;
11226                out.push(with_host(|h| h.str_of(&s)));
11227            }
11228        }
11229    }
11230    Ok(out)
11231}
11232
11233/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11234/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11235/// the merge step; default order is by the string form of each element.
11236/// Propagates a comparator error.
11237///
11238/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11239/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11240/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11241/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11242/// number of times it is called is.
11243pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11244    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11245    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11246    // invoke path and reporting the generic `null is not a function`.
11247    let cmp = match cmp {
11248        Some(Value::Undef) => None,
11249        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11250            // V8 renders the offending value with `NoSideEffectsToString`, not
11251            // with `util.inspect`: a string appears bare (`: x`) rather than
11252            // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11253            let shown = no_side_effects_string(v);
11254            return Err(host::type_error(&format!(
11255                "The comparison function must be either a function or undefined: {shown}"
11256            )));
11257        }
11258        other => other,
11259    };
11260    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11261    // comparator — it sorts to the end after the defined values are ordered.
11262    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11263    // node v26.7.0; the insertion sort called the comparator twice, on
11264    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11265    // is `undefined`, so swapping keeps the defined values in input order.
11266    let mut defined = 0;
11267    for i in 0..items.len() {
11268        if !matches!(items[i], Value::Undef) {
11269            items.swap(defined, i);
11270            defined += 1;
11271        }
11272    }
11273    merge_sort(&mut items[..defined], cmp)
11274}
11275
11276/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11277/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11278/// `> 0` and the pair keeps its input order.
11279fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11280    match cmp {
11281        Some(cb) => {
11282            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11283            Ok(with_host(|h| h.to_number(&v)))
11284        }
11285        None => {
11286            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11287            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11288            // from Rust's `String` order off the BMP.
11289            let x = with_host(|h| h.str_of(a));
11290            let y = with_host(|h| h.str_of(b));
11291            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11292                Ok(1.0)
11293            } else {
11294                Ok(-1.0)
11295            }
11296        }
11297    }
11298}
11299
11300/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11301/// array cannot walk the native stack the JS comparator also runs on, and the
11302/// two buffers are swapped each pass instead of copied back.
11303fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11304    let n = items.len();
11305    if n < 2 {
11306        return Ok(());
11307    }
11308    let mut src = items.to_vec();
11309    let mut dst = src.clone();
11310    let mut width = 1;
11311    while width < n {
11312        let mut lo = 0;
11313        while lo < n {
11314            let mid = (lo + width).min(n);
11315            let hi = (lo + 2 * width).min(n);
11316            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11317            lo = hi;
11318        }
11319        std::mem::swap(&mut src, &mut dst);
11320        width *= 2;
11321    }
11322    items.clone_from_slice(&src);
11323    Ok(())
11324}
11325
11326/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11327/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11328/// keeps the two `k:0` entries in input order, as node does.
11329fn merge(
11330    left: &[Value],
11331    right: &[Value],
11332    out: &mut [Value],
11333    cmp: Option<&Value>,
11334) -> Result<(), String> {
11335    let (mut i, mut j, mut k) = (0, 0, 0);
11336    while i < left.len() && j < right.len() {
11337        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11338            out[k] = right[j].clone();
11339            j += 1;
11340        } else {
11341            out[k] = left[i].clone();
11342            i += 1;
11343        }
11344        k += 1;
11345    }
11346    for v in left[i..].iter().chain(&right[j..]) {
11347        out[k] = v.clone();
11348        k += 1;
11349    }
11350    Ok(())
11351}
11352
11353/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11354/// f64 so `Infinity` (full flatten) and finite counts share one path.
11355///
11356/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11357/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11358/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11359/// stack floor the VM does, so the answer is a catchable error rather than the
11360/// `fatal runtime error: stack overflow` abort this used to produce.
11361/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11362/// elements because each level tests `HasProperty` before recursing, so a hole
11363/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11364fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11365    if host::stack_exhausted() {
11366        return Err(host::stack_overflow_error());
11367    }
11368    let items = array_items(src);
11369    let holes = absent_set(src);
11370    for (i, it) in items.into_iter().enumerate() {
11371        if holes.contains(&i) {
11372            continue;
11373        }
11374        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11375        if nested {
11376            flatten_into(&it, depth - 1.0, out)?;
11377        } else {
11378            out.push(it);
11379        }
11380    }
11381    Ok(())
11382}
11383
11384fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11385    let len = array_len(recv);
11386    let start = {
11387        let s = arg_num(&args, 0);
11388        if s < 0.0 {
11389            ((len as f64 + s).max(0.0)) as usize
11390        } else {
11391            (s as usize).min(len)
11392        }
11393    };
11394    let delete = if args.len() >= 2 {
11395        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11396    } else {
11397        len - start
11398    };
11399    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11400    let inserted = inserts.len();
11401    // The receiver's holes shift by (inserted - deleted) past the cut, and the
11402    // ones inside the cut move into the RETURNED array at their offset there.
11403    let holes = hole_set(recv);
11404    let removed = with_host(|h| {
11405        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11406            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11407            removed
11408        } else {
11409            Vec::new()
11410        }
11411    });
11412    let spliced = with_host(|h| {
11413        h.install_holes(
11414            recv,
11415            holes
11416                .iter()
11417                .filter_map(|&i| {
11418                    if i < start {
11419                        Some(i)
11420                    } else if i < start + delete {
11421                        None
11422                    } else {
11423                        Some(i - delete + inserted)
11424                    }
11425                })
11426                .collect(),
11427        );
11428        (removed, holes.clone())
11429    });
11430    // The REMOVED elements come back as an array of the receiver's species
11431    // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11432    let (removed, holes) = spliced;
11433    let out = array_species_create(recv, removed)?;
11434    with_host(|h| {
11435        h.install_holes(
11436            &out,
11437            holes
11438                .iter()
11439                .filter(|&&i| i >= start && i < start + delete)
11440                .map(|&i| i - start)
11441                .collect(),
11442        );
11443    });
11444    Ok(out)
11445}
11446
11447fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11448    let norm = |v: f64| -> usize {
11449        if v < 0.0 {
11450            ((len as f64 + v).max(0.0)) as usize
11451        } else {
11452            (v as usize).min(len)
11453        }
11454    };
11455    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11456        0
11457    } else {
11458        norm(arg_num(args, 0))
11459    };
11460    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11461        len
11462    } else {
11463        norm(arg_num(args, 1))
11464    };
11465    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11466    // never a reversed one: JS `slice` clamps `end` up to `start`.
11467    (lo, hi.max(lo))
11468}
11469
11470/// The argument positions each `String.prototype` method coerces with
11471/// `ToNumber` rather than `ToString`. Everything not listed is a string
11472/// position — which matters only for a SYMBOL argument, the one value both
11473/// conversions refuse, and refuse with different wording.
11474///
11475/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11476/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11477/// pair the other way round (a length then a pad string).
11478const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11479    ("at", &[0]),
11480    ("charAt", &[0]),
11481    ("charCodeAt", &[0]),
11482    ("codePointAt", &[0]),
11483    ("endsWith", &[1]),
11484    ("includes", &[1]),
11485    ("indexOf", &[1]),
11486    ("lastIndexOf", &[1]),
11487    ("padEnd", &[0]),
11488    ("padStart", &[0]),
11489    ("repeat", &[0]),
11490    ("slice", &[0, 1]),
11491    ("split", &[1]),
11492    ("startsWith", &[1]),
11493    ("substr", &[0, 1]),
11494    ("substring", &[0, 1]),
11495];
11496
11497/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11498/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11499/// rendered `Symbol(d)` into the result — silently, which is the shape of
11500/// mistake that makes a symbol key leak into text.
11501fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11502    let numeric = STRING_METHOD_NUMERIC_ARGS
11503        .iter()
11504        .find(|(m, _)| *m == name)
11505        .map(|(_, ps)| *ps)
11506        .unwrap_or(&[]);
11507    for (i, a) in args.iter().enumerate() {
11508        if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11509            let kind = if numeric.contains(&i) {
11510                "number"
11511            } else {
11512                "string"
11513            };
11514            return Err(host::type_error(&format!(
11515                "Cannot convert a Symbol value to a {kind}"
11516            )));
11517        }
11518    }
11519    Ok(())
11520}
11521
11522/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11523/// reads them: a numeric position through `ToNumber`, every other through
11524/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11525/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11526/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11527///
11528/// The positions that must NOT be coerced are the ones with their own protocol:
11529/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11530/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11531/// `replace`/`replaceAll`. Each of those already has a path that handles the
11532/// value as an object, and stringifying it first would take that path away.
11533/// The argument positions each `Array.prototype` method coerces with
11534/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11535/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11536/// are stored as given, and `indexOf`/`includes` compare their first argument
11537/// without converting it.
11538const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11539    ("at", &[0]),
11540    ("copyWithin", &[0, 1, 2]),
11541    ("fill", &[1, 2]),
11542    ("flat", &[0]),
11543    ("includes", &[1]),
11544    ("indexOf", &[1]),
11545    ("lastIndexOf", &[1]),
11546    ("slice", &[0, 1]),
11547    ("splice", &[0, 1]),
11548    ("toSpliced", &[0, 1]),
11549    ("with", &[0]),
11550];
11551
11552/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11553/// number, and is deliberately absent.
11554const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11555    ("toExponential", &[0]),
11556    ("toFixed", &[0]),
11557    ("toPrecision", &[0]),
11558    ("toString", &[0]),
11559];
11560
11561/// Replace the listed argument positions with their `ToNumber` value, running a
11562/// user `valueOf` and propagating a throw from it. Every one of these read the
11563/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11564/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11565/// was a RangeError.
11566/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11567fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11568    let v = args.get(i).cloned().unwrap_or(Value::Undef);
11569    let p = host::to_primitive(&v, "number")?;
11570    Ok(with_host(|h| h.to_number(&p)))
11571}
11572
11573fn coerce_numeric_args(
11574    table: &[(&str, &[usize])],
11575    name: &str,
11576    mut args: Vec<Value>,
11577) -> Result<Vec<Value>, String> {
11578    let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11579        return Ok(args);
11580    };
11581    for &i in *positions {
11582        let Some(a) = args.get(i) else { continue };
11583        if matches!(a, Value::Undef) {
11584            continue;
11585        }
11586        let p = host::to_primitive(a, "number")?;
11587        args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11588    }
11589    Ok(args)
11590}
11591
11592/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11593/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11594/// matches at position 0.
11595fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11596    let src = if matches!(v, Value::Undef) {
11597        String::new()
11598    } else {
11599        with_host(|h| h.str_of(v))
11600    };
11601    let fv = with_host(|h| h.new_str(flags.to_string()));
11602    let sv = with_host(|h| h.new_str(src));
11603    regexp_ctor(&[sv, fv])
11604}
11605
11606fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11607    let numeric = STRING_METHOD_NUMERIC_ARGS
11608        .iter()
11609        .find(|(m, _)| *m == name)
11610        .map(|(_, ps)| *ps)
11611        .unwrap_or(&[]);
11612    let protocol = match name {
11613        "replace" | "replaceAll" => Some("@@replace"),
11614        "split" => Some("@@split"),
11615        "match" => Some("@@match"),
11616        "matchAll" => Some("@@matchAll"),
11617        "search" => Some("@@search"),
11618        // These three do not CONSUME `Symbol.match`, they reject a value that
11619        // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11620        // the object intact so that check still sees one; stringifying first
11621        // turned the TypeError into an ordinary search.
11622        "startsWith" | "endsWith" | "includes" => Some("@@match"),
11623        _ => None,
11624    };
11625    let mut out = Vec::with_capacity(args.len());
11626    for (i, a) in args.into_iter().enumerate() {
11627        if matches!(a, Value::Undef) {
11628            out.push(a);
11629            continue;
11630        }
11631        if numeric.contains(&i) {
11632            let p = host::to_primitive(&a, "number")?;
11633            out.push(Value::Float(with_host(|h| h.to_number(&p))));
11634            continue;
11635        }
11636        // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11637        // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11638        // regexp and coerces like anything else. The consuming protocols use
11639        // `GetMethod`, which additionally requires a callable.
11640        let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11641        let carries = |p: &str| match host::protocol_lookup(&a, p) {
11642            Ok(Some(m)) => {
11643                if is_regexp_like {
11644                    with_host(|h| h.truthy(&m))
11645                } else {
11646                    with_host(|h| host::is_callable(h, &m))
11647                }
11648            }
11649            _ => false,
11650        };
11651        let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11652            || (i == 0 && protocol.is_some_and(carries))
11653            || (i == 1
11654                && matches!(name, "replace" | "replaceAll")
11655                && with_host(|h| host::is_callable(h, &a)));
11656        if exempt {
11657            out.push(a);
11658            continue;
11659        }
11660        out.push(host::to_string_value(&a)?);
11661    }
11662    Ok(out)
11663}
11664
11665fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11666    reject_symbol_args(name, &args)?;
11667    let args = coerce_string_args(name, args)?;
11668    // Every index-bearing method below counts UTF-16 code units, so they all
11669    // work off this one decoding rather than off `s.chars()` (code points),
11670    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11671    let u = crate::utf16::Units::of(s);
11672    match name {
11673        // `for…of` / spread over a string iterates CODE POINTS, not code units:
11674        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11675        // is the one string operation that is specified in chars, so it stays
11676        // on `s.chars()` on purpose — do not "fix" it to match the others.
11677        "@@iterator" => {
11678            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11679            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11680        }
11681        "toUpperCase" => Ok(new_s(s.to_uppercase())),
11682        "toLowerCase" => Ok(new_s(s.to_lowercase())),
11683        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11684        // from the plain forms only for the locale-specific mappings (Turkish
11685        // dotless i, Lithuanian accents); with no locale argument they are the
11686        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11687        // `to_lowercase`. They threw `is not a function` before, so the common
11688        // no-argument call — the only form this runtime can answer, since it
11689        // carries no ICU — failed outright rather than agreeing with node.
11690        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11691        // is `'i'` here and `'ı'` in node.
11692        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11693        // has no locale rendering. Missing it made an ARRAY of strings fail too,
11694        // since `Array.prototype.toLocaleString` invokes it per element.
11695        "toLocaleString" => Ok(new_s(s.to_string())),
11696        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11697        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11698        // Locale comparison (ASCII approximation of ICU collation): primary by
11699        // case-folded order, then lowercase sorts before uppercase at a tie.
11700        "localeCompare" => {
11701            let other = with_host(|h| h.str_of(&arg0(&args)));
11702            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11703            let r = match la.cmp(&lb) {
11704                std::cmp::Ordering::Less => -1.0,
11705                std::cmp::Ordering::Greater => 1.0,
11706                std::cmp::Ordering::Equal => {
11707                    let mut t = 0.0;
11708                    for (ca, cb) in s.chars().zip(other.chars()) {
11709                        if ca != cb {
11710                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11711                            break;
11712                        }
11713                    }
11714                    t
11715                }
11716            };
11717            Ok(Value::Float(r))
11718        }
11719        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11720        //
11721        // This used to return the receiver unchanged and only validate the FORM
11722        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11723        // one code point) and `"Å"` (NFD, two) stayed distinct under
11724        // `.normalize()`, so the standard way to compare Unicode text for
11725        // canonical equivalence silently answered `false`, and `NFKC` never
11726        // folded a compatibility character (`"fi"` stayed one code point instead
11727        // of becoming `"fi"`). The tables come from `unicode-normalization`.
11728        "normalize" => {
11729            use unicode_normalization::UnicodeNormalization;
11730            let form = match args.first() {
11731                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11732                _ => "NFC".to_string(),
11733            };
11734            let out = match form.as_str() {
11735                "NFC" => s.nfc().collect::<String>(),
11736                "NFD" => s.nfd().collect::<String>(),
11737                "NFKC" => s.nfkc().collect::<String>(),
11738                "NFKD" => s.nfkd().collect::<String>(),
11739                _ => {
11740                    return Err(host::range_error(
11741                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11742                    ))
11743                }
11744            };
11745            Ok(new_s(out))
11746        }
11747        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11748        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11749        // value this runtime can hold is well-formed by construction and
11750        // `toWellFormed` has nothing to replace. Both answers are therefore
11751        // exact for every string that survives storage; the one case node
11752        // answers differently is a surrogate half extracted by `charAt`/`slice`,
11753        // which is already `U+FFFD` here — the documented lone-surrogate
11754        // boundary in `utf16`, not a separate gap.
11755        "isWellFormed" => Ok(Value::Bool(true)),
11756        "toWellFormed" => Ok(new_s(s.to_string())),
11757        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11758        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11759        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11760        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11761        "toString" | "valueOf" => Ok(new_s(s.to_string())),
11762        "charAt" => {
11763            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11764            Ok(new_s(at.unwrap_or_default()))
11765        }
11766        "at" => {
11767            let n = arg_num(&args, 0);
11768            // A negative position counts back from the end; `NaN` is 0. An
11769            // infinite position is out of range in either direction.
11770            let i = if n.is_nan() {
11771                Some(0i64)
11772            } else if n.is_finite() {
11773                let i = n.trunc() as i64;
11774                Some(if i < 0 { i + u.len() as i64 } else { i })
11775            } else {
11776                None
11777            };
11778            match i
11779                .and_then(|i| usize::try_from(i).ok())
11780                .and_then(|i| u.unit_str(i))
11781            {
11782                Some(c) => Ok(new_s(c)),
11783                None => Ok(Value::Undef),
11784            }
11785        }
11786        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11787        // astral character, not the character. `codePointAt` looks ahead one
11788        // unit and reports the whole scalar when the pair is well formed. They
11789        // agree everywhere on the BMP, which is why they used to share an arm.
11790        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11791        // `codePointAt` yields `undefined` (measured on node v26.7.0).
11792        "charCodeAt" => {
11793            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11794            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11795        }
11796        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11797            Some(cp) => Ok(Value::Float(f64::from(cp))),
11798            None => Ok(Value::Undef),
11799        },
11800        // The search quartet all honor their optional position argument.
11801        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11802        // parameterCount walks a query string with exactly that call.
11803        "indexOf" => {
11804            let needle = needle_units(&args);
11805            let from = clamp_pos(arg_num(&args, 1), u.len());
11806            Ok(Value::Float(
11807                search_from(u.as_slice(), needle.as_slice(), from)
11808                    .map(|i| i as f64)
11809                    .unwrap_or(-1.0),
11810            ))
11811        }
11812        "lastIndexOf" => {
11813            let needle = needle_units(&args);
11814            // An absent or NaN position means "search the whole string".
11815            let n = arg_num(&args, 1);
11816            let upto = if n.is_nan() {
11817                u.len()
11818            } else {
11819                clamp_pos(n, u.len())
11820            };
11821            Ok(Value::Float(
11822                search_last(u.as_slice(), needle.as_slice(), upto)
11823                    .map(|i| i as f64)
11824                    .unwrap_or(-1.0),
11825            ))
11826        }
11827        // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11828        // argument outright, and `IsRegExp` is what decides — so an object
11829        // advertising `Symbol.match` is rejected too. None of them checked.
11830        "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11831            Err(host::type_error(&format!(
11832                "First argument to String.prototype.{name} must not be a regular expression"
11833            )))
11834        }
11835        "includes" => {
11836            let needle = needle_units(&args);
11837            let from = clamp_pos(arg_num(&args, 1), u.len());
11838            Ok(Value::Bool(
11839                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11840            ))
11841        }
11842        "startsWith" => {
11843            let needle = needle_units(&args);
11844            let from = clamp_pos(arg_num(&args, 1), u.len());
11845            Ok(Value::Bool(
11846                u.as_slice()[from..].starts_with(needle.as_slice()),
11847            ))
11848        }
11849        "endsWith" => {
11850            let needle = needle_units(&args);
11851            // The 2nd argument is where the string is treated as ENDING.
11852            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11853                u.len()
11854            } else {
11855                clamp_pos(arg_num(&args, 1), u.len())
11856            };
11857            Ok(Value::Bool(
11858                u.as_slice()[..end].ends_with(needle.as_slice()),
11859            ))
11860        }
11861        "slice" => {
11862            let (lo, hi) = slice_bounds(&args, u.len());
11863            Ok(new_s(u.slice(lo, hi)))
11864        }
11865        "substring" => {
11866            let mut a = arg_num(&args, 0).max(0.0) as usize;
11867            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11868                u.len()
11869            } else {
11870                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11871            };
11872            a = a.min(u.len());
11873            if a > b {
11874                std::mem::swap(&mut a, &mut b);
11875            }
11876            Ok(new_s(u.slice(a, b)))
11877        }
11878        "substr" => {
11879            // A negative start counts from the end: max(len + start, 0).
11880            let len = u.len() as i64;
11881            let mut start = arg_num(&args, 0) as i64;
11882            if start < 0 {
11883                start = (len + start).max(0);
11884            }
11885            let start = (start as usize).min(u.len());
11886            let count = if args.len() >= 2 {
11887                arg_num(&args, 1).max(0.0) as usize
11888            } else {
11889                u.len()
11890            };
11891            let end = start.saturating_add(count).min(u.len());
11892            Ok(new_s(u.slice(start, end)))
11893        }
11894        "repeat" => {
11895            let n = arg_num(&args, 0);
11896            // `RangeError`, not `TypeError`, and the count is named:
11897            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11898            if n < 0.0 || !n.is_finite() {
11899                return Err(host::range_error(&format!(
11900                    "Invalid count value: {}",
11901                    host::fmt_number(n)
11902                )));
11903            }
11904            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11905            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11906            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11907            // is `RangeError: Invalid string length`.
11908            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11909                return Err(host::invalid_string_length());
11910            }
11911            Ok(new_s(s.repeat(n as usize)))
11912        }
11913        "concat" => {
11914            let mut out = s.to_string();
11915            for a in &args {
11916                out.push_str(&with_host(|h| h.str_of(a)));
11917            }
11918            Ok(new_s(out))
11919        }
11920        "padStart" => Ok(new_s(pad(s, &args, true)?)),
11921        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11922        // Regex-taking string methods: dispatch to the regexp module when the
11923        // argument is a RegExp; otherwise keep the plain-string behavior.
11924        // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11925        // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11926        // though a RegExp does define that method. Delegating first skipped the
11927        // check and silently did a single replacement.
11928        "replaceAll"
11929            if is_regexp_arg(&arg0(&args))
11930                && !with_host(
11931                    |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11932                ) =>
11933        {
11934            Err(host::type_error(
11935                "String.prototype.replaceAll called with a non-global RegExp argument",
11936            ))
11937        }
11938        "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11939            if symbol_protocol(
11940                &arg0(&args),
11941                match name {
11942                    "match" => "@@match",
11943                    "matchAll" => "@@matchAll",
11944                    "search" => "@@search",
11945                    "split" => "@@split",
11946                    _ => "@@replace",
11947                },
11948            )
11949            .is_some() =>
11950        {
11951            let sym = match name {
11952                "match" => "@@match",
11953                "matchAll" => "@@matchAll",
11954                "search" => "@@search",
11955                "split" => "@@split",
11956                _ => "@@replace",
11957            };
11958            let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11959            let sv = with_host(|h| h.new_str(s.to_string()));
11960            let mut rest = vec![sv];
11961            rest.extend(args.iter().skip(1).cloned());
11962            host::invoke(&f, rest, Some(arg0(&args)))
11963        }
11964        // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11965        // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11966        // answered `null` for every string argument, which reads as "no match"
11967        // — the one answer a caller cannot tell from a real failure.
11968        // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11969        "match" => {
11970            let a = arg0(&args);
11971            let re = if is_regexp_arg(&a) {
11972                a
11973            } else {
11974                regexp_from_arg(&a, "")?
11975            };
11976            crate::regexp::str_match(s, &re)
11977        }
11978        "matchAll" => {
11979            let a = arg0(&args);
11980            let re = if is_regexp_arg(&a) {
11981                a
11982            } else {
11983                regexp_from_arg(&a, "g")?
11984            };
11985            crate::regexp::str_match_all(s, &re)
11986        }
11987        "search" => {
11988            if is_regexp_arg(&arg0(&args)) {
11989                crate::regexp::str_search(s, &arg0(&args))
11990            } else {
11991                // 22.1.3.17 builds a RegExp from the argument, so a
11992                // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11993                // 1. The substring approximation this replaces agreed only for
11994                // a literal needle, and answered -1 for an absent argument
11995                // where the empty pattern matches at 0.
11996                let re = regexp_from_arg(&arg0(&args), "")?;
11997                crate::regexp::str_search(s, &re)
11998            }
11999        }
12000        "replace" => {
12001            let pat = arg0(&args);
12002            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12003            if is_regexp_arg(&pat) {
12004                crate::regexp::str_replace_regex(s, &pat, &repl, false)
12005            } else if with_host(|h| host::is_callable(h, &repl)) {
12006                Ok(new_s(replace_str_fn(
12007                    s,
12008                    &with_host(|h| h.str_of(&pat)),
12009                    &repl,
12010                    false,
12011                )?))
12012            } else {
12013                let from = with_host(|h| h.str_of(&pat));
12014                let to = with_host(|h| h.str_of(&repl));
12015                Ok(new_s(replace_str_plain(s, &from, &to, false)))
12016            }
12017        }
12018        "replaceAll" => {
12019            let pat = arg0(&args);
12020            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12021            if is_regexp_arg(&pat) {
12022                // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12023                // because `replaceAll` cannot honour "all" without `g`. This
12024                // used to replace only the first match and say nothing.
12025                let global = with_host(|h| match h.get(&pat) {
12026                    Some(JsObj::RegExp(r)) => r.global,
12027                    _ => true,
12028                });
12029                if !global {
12030                    return Err(host::type_error(
12031                        "String.prototype.replaceAll called with a non-global RegExp argument",
12032                    ));
12033                }
12034                crate::regexp::str_replace_regex(s, &pat, &repl, true)
12035            } else if with_host(|h| host::is_callable(h, &repl)) {
12036                Ok(new_s(replace_str_fn(
12037                    s,
12038                    &with_host(|h| h.str_of(&pat)),
12039                    &repl,
12040                    true,
12041                )?))
12042            } else {
12043                let from = with_host(|h| h.str_of(&pat));
12044                let to = with_host(|h| h.str_of(&repl));
12045                Ok(new_s(replace_str_plain(s, &from, &to, true)))
12046            }
12047        }
12048        "split" => {
12049            if is_regexp_arg(&arg0(&args)) {
12050                let limit = args
12051                    .get(1)
12052                    .filter(|v| !matches!(v, Value::Undef))
12053                    .map(|v| with_host(|h| h.to_number(v)) as usize);
12054                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12055            }
12056            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12057                vec![new_s(s.to_string())]
12058            } else {
12059                let sep = with_host(|h| h.str_of(&args[0]));
12060                if sep.is_empty() {
12061                    // `split('')` yields one element per code UNIT, so an astral
12062                    // character becomes its two surrogate halves.
12063                    (0..u.len())
12064                        .filter_map(|i| u.unit_str(i))
12065                        .map(new_s)
12066                        .collect()
12067                } else {
12068                    s.split(&sep as &str)
12069                        .map(|p| new_s(p.to_string()))
12070                        .collect()
12071                }
12072            };
12073            // Optional limit: keep at most `limit` substrings.
12074            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12075                let n = with_host(|h| h.to_number(lim));
12076                if n.is_finite() && n >= 0.0 {
12077                    parts.truncate(n as usize);
12078                }
12079            }
12080            Ok(with_host(|h| h.new_array(parts)))
12081        }
12082        _ => Err(host::type_error(&format!("{name} is not a function"))),
12083    }
12084}
12085
12086/// GetSubstitution (22.1.3.19) for a STRING search value.
12087///
12088/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12089/// the pattern is a regexp or a plain string, but the string path here did a
12090/// raw `str::replace` and passed the template through verbatim — so
12091/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12092/// regexp path has always expanded them.
12093///
12094/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12095/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12096/// what node does.
12097fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12098    let chars: Vec<char> = templ.chars().collect();
12099    let mut out = String::new();
12100    let mut i = 0;
12101    while i < chars.len() {
12102        if chars[i] == '$' && i + 1 < chars.len() {
12103            match chars[i + 1] {
12104                '$' => {
12105                    out.push('$');
12106                    i += 2;
12107                    continue;
12108                }
12109                '&' => {
12110                    out.push_str(matched);
12111                    i += 2;
12112                    continue;
12113                }
12114                '`' => {
12115                    out.push_str(&subject[..position]);
12116                    i += 2;
12117                    continue;
12118                }
12119                '\'' => {
12120                    out.push_str(&subject[position + matched.len()..]);
12121                    i += 2;
12122                    continue;
12123                }
12124                _ => {}
12125            }
12126        }
12127        out.push(chars[i]);
12128        i += 1;
12129    }
12130    out
12131}
12132
12133/// `replace`/`replaceAll` with a string pattern and a string replacement,
12134/// expanding each match's `$` patterns against its own position.
12135fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12136    if from.is_empty() && !all {
12137        return format!("{}{s}", substitute_plain(to, "", 0, s));
12138    }
12139    let mut out = String::new();
12140    let mut rest = 0usize;
12141    while let Some(rel) = s[rest..].find(from) {
12142        let at = rest + rel;
12143        out.push_str(&s[rest..at]);
12144        out.push_str(&substitute_plain(to, from, at, s));
12145        rest = at + from.len();
12146        if !all {
12147            break;
12148        }
12149        // An empty pattern matches between every character; step one along so
12150        // the scan terminates.
12151        if from.is_empty() {
12152            if rest >= s.len() {
12153                break;
12154            }
12155            let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12156            out.push_str(&s[rest..rest + step]);
12157            rest += step;
12158        }
12159    }
12160    out.push_str(&s[rest..]);
12161    out
12162}
12163
12164fn new_s(s: String) -> Value {
12165    with_host(|h| h.new_str(s))
12166}
12167
12168/// Where a forward `indexOf`/`includes` search starts, given the optional
12169/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12170/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12171/// or past the end finds nothing, which callers report as `-1` / `false`.
12172pub(crate) fn search_start(n: f64, len: usize) -> usize {
12173    if n.is_nan() {
12174        return 0;
12175    }
12176    let n = n.trunc();
12177    if n >= 0.0 {
12178        if n >= len as f64 {
12179            len
12180        } else {
12181            n as usize
12182        }
12183    } else {
12184        let from_end = len as f64 + n;
12185        if from_end <= 0.0 {
12186            0
12187        } else {
12188            from_end as usize
12189        }
12190    }
12191}
12192
12193/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12194/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12195/// the last element — which is why this takes an `Option` rather than reading
12196/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12197/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12198pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12199    if len == 0 {
12200        return None;
12201    }
12202    let n = match from {
12203        None => return Some(len - 1),
12204        Some(v) if v.is_nan() => 0.0,
12205        Some(v) => v.trunc(),
12206    };
12207    if n >= 0.0 {
12208        Some(if n >= len as f64 { len - 1 } else { n as usize })
12209    } else {
12210        let k = len as f64 + n;
12211        if k < 0.0 {
12212            None
12213        } else {
12214            Some(k as usize)
12215        }
12216    }
12217}
12218
12219/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12220/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12221fn clamp_pos(n: f64, len: usize) -> usize {
12222    if n.is_nan() || n <= 0.0 {
12223        0
12224    } else if n >= len as f64 {
12225        len
12226    } else {
12227        n.trunc() as usize
12228    }
12229}
12230
12231/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12232/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12233/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12234fn unit_pos(n: f64) -> Option<usize> {
12235    if n.is_nan() {
12236        Some(0)
12237    } else if n < 0.0 || !n.is_finite() {
12238        None
12239    } else {
12240        Some(n.trunc() as usize)
12241    }
12242}
12243
12244/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12245/// the needle is compared in the same alphabet the haystack is indexed by.
12246fn needle_units(args: &[Value]) -> crate::utf16::Units {
12247    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12248}
12249
12250/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12251/// needle matches at `from` itself, as JS specifies.
12252fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12253    if needle.is_empty() {
12254        return Some(from.min(hay.len()));
12255    }
12256    if needle.len() > hay.len() {
12257        return None;
12258    }
12259    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12260}
12261
12262/// The highest index `<= upto` at which `needle` occurs in `hay`.
12263fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12264    if needle.is_empty() {
12265        return Some(upto.min(hay.len()));
12266    }
12267    if needle.len() > hay.len() {
12268        return None;
12269    }
12270    let last = hay.len() - needle.len();
12271    (0..=upto.min(last))
12272        .rev()
12273        .find(|&i| &hay[i..i + needle.len()] == needle)
12274}
12275
12276fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12277    let target_f = arg_num(args, 0);
12278    let target = if target_f.is_finite() && target_f > 0.0 {
12279        target_f as usize
12280    } else {
12281        0
12282    };
12283    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12284    // is `'-𝒳'` in node, not `'--𝒳'`.
12285    let cur = crate::utf16::len(s);
12286    if cur >= target {
12287        return Ok(s.to_string());
12288    }
12289    let filler = if args.len() >= 2 {
12290        with_host(|h| h.str_of(&args[1]))
12291    } else {
12292        " ".to_string()
12293    };
12294    if filler.is_empty() {
12295        return Ok(s.to_string());
12296    }
12297    // Checked only AFTER the two short-circuits, which is the order V8 uses:
12298    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12299    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12300    if target_f > host::MAX_STRING_LENGTH as f64 {
12301        return Err(host::invalid_string_length());
12302    }
12303    let need = target - cur;
12304    let fill = crate::utf16::Units::of(&filler);
12305    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12306    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12307    // (see src/utf16.rs).
12308    let units: Vec<u16> = (0..need)
12309        .filter_map(|i| fill.unit(i % fill.len()))
12310        .collect();
12311    let padding = crate::utf16::to_string_lossy(&units);
12312    Ok(if start {
12313        format!("{padding}{s}")
12314    } else {
12315        format!("{s}{padding}")
12316    })
12317}
12318
12319/// V8's radix rejection, shared by `Number.prototype.toString` and
12320/// `BigInt.prototype.toString` — one string, because they are one message and
12321/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12322/// must be").
12323const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12324
12325/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12326fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12327    match name {
12328        "toString" => {
12329            let radix = match args.first() {
12330                None | Some(Value::Undef) => 10,
12331                Some(_) => {
12332                    let t = arg_num(&args, 0).trunc();
12333                    if !(2.0..=36.0).contains(&t) {
12334                        return Err(host::range_error(RADIX_RANGE));
12335                    }
12336                    t as u32
12337                }
12338            };
12339            Ok(new_s(b.to_str_radix(radix)))
12340        }
12341        // `BigInt.prototype.toLocaleString` groups thousands like the Number
12342        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12343        // returning the bare digits made it the only numeric type that skipped
12344        // grouping. Same en-US-shaped output as `Number.prototype`; the
12345        // `locales`/`options` arguments are ignored (no ICU here).
12346        "toLocaleString" => {
12347            let digits = b.magnitude().to_string();
12348            let sign = if b.sign() == num_bigint::Sign::Minus {
12349                "-"
12350            } else {
12351                ""
12352            };
12353            Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12354        }
12355        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12356        _ => Err(host::type_error(&format!("{name} is not a function"))),
12357    }
12358}
12359
12360fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12361    let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12362    match name {
12363        "toFixed" => {
12364            let digits = arg_num(&args, 0);
12365            if !(0.0..=100.0).contains(&digits.trunc()) {
12366                return Err(host::range_error(
12367                    "toFixed() digits argument must be between 0 and 100",
12368                ));
12369            }
12370            Ok(new_s(to_fixed(n, digits as usize)))
12371        }
12372        "toExponential" => {
12373            // `undefined` (or a missing argument) selects the shortest form.
12374            let f = match args.first() {
12375                None | Some(Value::Undef) => None,
12376                Some(_) => {
12377                    let d = arg_num(&args, 0).trunc();
12378                    if !(0.0..=100.0).contains(&d) {
12379                        return Err(host::range_error(
12380                            "toExponential() argument must be between 0 and 100",
12381                        ));
12382                    }
12383                    Some(d as usize)
12384                }
12385            };
12386            Ok(new_s(to_exponential(n, f)))
12387        }
12388        "toString" => {
12389            // An out-of-range radix THROWS; it does not silently fall back to
12390            // base 10. `(1).toString(37)` returned "1" here, so a support probe
12391            // was told every radix worked.
12392            let radix = match args.first() {
12393                None | Some(Value::Undef) => 10,
12394                Some(_) => {
12395                    let r = arg_num(&args, 0);
12396                    let t = r.trunc();
12397                    if !(2.0..=36.0).contains(&t) {
12398                        return Err(host::range_error(RADIX_RANGE));
12399                    }
12400                    t as u32
12401                }
12402            };
12403            if radix == 10 {
12404                Ok(new_s(host::fmt_number(n)))
12405            } else {
12406                Ok(new_s(to_radix(n, radix)))
12407            }
12408        }
12409        "toPrecision" => {
12410            // `undefined` (or a missing argument) behaves like `toString()`.
12411            match args.first() {
12412                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12413                Some(_) => {
12414                    let p = arg_num(&args, 0).trunc();
12415                    if !(1.0..=100.0).contains(&p) {
12416                        return Err(host::range_error(
12417                            "toPrecision() argument must be between 1 and 100",
12418                        ));
12419                    }
12420                    Ok(new_s(to_precision(n, p as usize)))
12421                }
12422            }
12423        }
12424        "toLocaleString" => Ok(new_s(to_locale_string(n))),
12425        "valueOf" => Ok(Value::Float(n)),
12426        _ => Err(host::type_error(&format!("{name} is not a function"))),
12427    }
12428}
12429
12430/// `Number.prototype.toLocaleString()` with the default locale and options:
12431/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12432/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12433/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12434/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12435/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12436fn to_locale_string(n: f64) -> String {
12437    if n.is_nan() {
12438        return "NaN".to_string();
12439    }
12440    if n.is_infinite() {
12441        return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12442    }
12443    let neg = n.is_sign_negative();
12444    // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12445    // (and a bare trailing point). `to_fixed` rounds half away from zero.
12446    // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12447    // which is exponential — and the grouping below then chopped up the
12448    // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12449    // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12450    // source: node groups the shortest decimal form, so `(1e100)
12451    // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12452    // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12453    // deliberately different rule — see `bigint_ctor`.)
12454    let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12455    let trimmed = match fixed.split_once('.') {
12456        Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12457        None => fixed.as_str(),
12458    };
12459    let (int_part, frac_part) = match trimmed.split_once('.') {
12460        Some((i, f)) => (i, Some(f)),
12461        None => (trimmed, None),
12462    };
12463    let mut out = String::new();
12464    if neg {
12465        out.push('-'); // Intl keeps the sign even for -0.
12466    }
12467    out.push_str(&group_thousands(int_part));
12468    if let Some(f) = frac_part {
12469        out.push('.');
12470        out.push_str(f);
12471    }
12472    out
12473}
12474
12475/// Write a nonnegative decimal string in plain positional form, expanding an
12476/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12477/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12478/// negative exponent (a magnitude below 1, which the caller has already rounded
12479/// to zero), is returned unchanged.
12480fn expand_exponential(s: &str) -> String {
12481    let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12482        return s.to_string();
12483    };
12484    let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12485        return s.to_string();
12486    };
12487    if exp <= 0 {
12488        return s.to_string();
12489    }
12490    let (int_digits, frac_digits) = match mantissa.split_once('.') {
12491        Some((i, f)) => (i.to_string(), f.to_string()),
12492        None => (mantissa.to_string(), String::new()),
12493    };
12494    let mut digits = int_digits;
12495    digits.push_str(&frac_digits);
12496    // The exponent consumes the fractional digits first; whatever is left
12497    // becomes trailing zeros.
12498    let zeros = exp as usize - frac_digits.len().min(exp as usize);
12499    digits.push_str(&"0".repeat(zeros));
12500    digits
12501}
12502
12503/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12504fn group_thousands(int_part: &str) -> String {
12505    let bytes = int_part.as_bytes();
12506    let n = bytes.len();
12507    let mut out = String::with_capacity(n + n / 3);
12508    for (i, &b) in bytes.iter().enumerate() {
12509        if i > 0 && (n - i) % 3 == 0 {
12510            out.push(',');
12511        }
12512        out.push(b as char);
12513    }
12514    out
12515}
12516
12517/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12518/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12519/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12520/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12521///
12522/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12523/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12524/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12525fn to_fixed(n: f64, f: usize) -> String {
12526    if !n.is_finite() {
12527        return host::fmt_number(n);
12528    }
12529    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12530    if n.abs() >= 1e21 {
12531        return host::fmt_number(n);
12532    }
12533    let neg = n < 0.0;
12534    // Exact decimal with guard digits past the rounding position; then round the
12535    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12536    let full = format!("{:.*}", f + 25, n.abs());
12537    let mut body = round_decimal_string(&full, f);
12538    if neg {
12539        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12540    }
12541    body
12542}
12543
12544/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12545/// fractional digits, half away from zero, propagating carry across the point.
12546fn round_decimal_string(s: &str, f: usize) -> String {
12547    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12548    let mut digits: Vec<u8> = int_part
12549        .bytes()
12550        .chain(frac_part.bytes())
12551        .map(|b| b - b'0')
12552        .collect();
12553    let point = int_part.len(); // digits before the decimal point
12554    let keep = point + f; // number of leading digits to keep
12555
12556    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12557    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12558        let mut i = keep;
12559        loop {
12560            if i == 0 {
12561                digits.insert(0, 1);
12562                // A new leading digit shifts the decimal point right by one.
12563                return assemble_decimal(&digits, point + 1, f);
12564            }
12565            i -= 1;
12566            if digits[i] == 9 {
12567                digits[i] = 0;
12568            } else {
12569                digits[i] += 1;
12570                break;
12571            }
12572        }
12573    }
12574    assemble_decimal(&digits, point, f)
12575}
12576
12577/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12578/// `point` digits precede the decimal point.
12579fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12580    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12581    let int_str = int_str.trim_start_matches('0');
12582    let int_str = if int_str.is_empty() { "0" } else { int_str };
12583    if f == 0 {
12584        return int_str.to_string();
12585    }
12586    let frac: String = digits[point..point + f]
12587        .iter()
12588        .map(|d| (d + b'0') as char)
12589        .collect();
12590    format!("{int_str}.{frac}")
12591}
12592
12593/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12594/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12595/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12596/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12597/// exact digits are taken with guard positions and rounded here.
12598fn round_significant(a: f64, p: usize) -> (String, i32) {
12599    let sci = format!("{a:.*e}", p - 1 + 25);
12600    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12601    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12602    let all: Vec<u8> = mant
12603        .chars()
12604        .filter(|c| c.is_ascii_digit())
12605        .map(|c| c as u8 - b'0')
12606        .collect();
12607    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12608    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12609        // Round the p-digit mantissa up, propagating carry; a carry out of the
12610        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12611        let mut d: Vec<u8> = all[..p].to_vec();
12612        let mut i = p;
12613        loop {
12614            if i == 0 {
12615                d.insert(0, 1);
12616                d.truncate(p);
12617                e += 1;
12618                break;
12619            }
12620            i -= 1;
12621            if d[i] == 9 {
12622                d[i] = 0;
12623            } else {
12624                d[i] += 1;
12625                break;
12626            }
12627        }
12628        s = d.iter().map(|x| (x + b'0') as char).collect();
12629    }
12630    (s, e)
12631}
12632
12633/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12634/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12635/// `f` omitted, as many digits as uniquely identify the value are used
12636/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12637/// on the exact value, matching `toPrecision`.
12638fn to_exponential(n: f64, f: Option<usize>) -> String {
12639    if !n.is_finite() {
12640        return host::fmt_number(n);
12641    }
12642    let neg = n < 0.0;
12643    let a = n.abs();
12644    let (s, e) = if a == 0.0 {
12645        // Zero has no significant digits: emit "0" padded to the requested width.
12646        ("0".repeat(f.unwrap_or(0) + 1), 0)
12647    } else {
12648        match f {
12649            Some(f) => round_significant(a, f + 1),
12650            None => {
12651                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12652                let sci = format!("{a:e}");
12653                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12654                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12655                let trimmed = digits.trim_end_matches('0');
12656                let digits = if trimmed.is_empty() { "0" } else { trimmed };
12657                (digits.to_string(), exp_str.parse().unwrap_or(0))
12658            }
12659        }
12660    };
12661    let sign = if e >= 0 { '+' } else { '-' };
12662    let mag = e.abs();
12663    let body = if s.len() == 1 {
12664        format!("{s}e{sign}{mag}")
12665    } else {
12666        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12667    };
12668    if neg {
12669        format!("-{body}")
12670    } else {
12671        body
12672    }
12673}
12674
12675/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12676/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12677/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12678/// retained (`(100).toPrecision(5) === "100.00"`).
12679fn to_precision(n: f64, p: usize) -> String {
12680    if !n.is_finite() {
12681        return host::fmt_number(n);
12682    }
12683    if n == 0.0 {
12684        return if p == 1 {
12685            "0".into()
12686        } else {
12687            format!("0.{}", "0".repeat(p - 1))
12688        };
12689    }
12690    let neg = n < 0.0;
12691    let (s, e) = round_significant(n.abs(), p);
12692    let pp = p as i32;
12693
12694    let body = if e < -6 || e >= pp {
12695        // Exponential: first digit, optional '.rest', signed exponent.
12696        let sign = if e >= 0 { '+' } else { '-' };
12697        let mag = e.abs();
12698        if p == 1 {
12699            format!("{s}e{sign}{mag}")
12700        } else {
12701            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12702        }
12703    } else if e >= 0 {
12704        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12705        let ip = (e + 1) as usize;
12706        if ip == p {
12707            s
12708        } else {
12709            format!("{}.{}", &s[..ip], &s[ip..])
12710        }
12711    } else {
12712        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12713        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12714    };
12715    if neg {
12716        format!("-{body}")
12717    } else {
12718        body
12719    }
12720}
12721
12722/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12723/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12724/// is emitted exact, and fractional digits are produced up to the input double's
12725/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12726/// carry-over back into already-written digits (and into the integer part).
12727fn to_radix(n: f64, radix: u32) -> String {
12728    if !n.is_finite() {
12729        return host::fmt_number(n);
12730    }
12731    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12732    let rf = radix as f64;
12733    let neg = n < 0.0;
12734    let value = n.abs();
12735
12736    let mut integer = value.floor();
12737    let mut fraction = value - integer;
12738
12739    // Fraction digits, most-significant first.
12740    let mut frac: Vec<u8> = Vec::new();
12741    // Only compute fractional digits down to the input double's precision.
12742    let mut delta = 0.5 * (next_up(value) - value);
12743    delta = delta.max(next_up(0.0));
12744    if fraction >= delta {
12745        loop {
12746            // Shift up by one digit.
12747            fraction *= rf;
12748            delta *= rf;
12749            let digit = fraction as usize;
12750            frac.push(digits[digit]);
12751            fraction -= digit as f64;
12752            // Round to even.
12753            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12754                // Carry-over: back-trace already-written fraction digits.
12755                loop {
12756                    match frac.pop() {
12757                        None => {
12758                            // Carried past the point into the integer part.
12759                            integer += 1.0;
12760                            break;
12761                        }
12762                        Some(c) => {
12763                            let d = if c > b'9' {
12764                                (c - b'a' + 10) as u32
12765                            } else {
12766                                (c - b'0') as u32
12767                            };
12768                            if d + 1 < radix {
12769                                frac.push(digits[(d + 1) as usize]);
12770                                break;
12771                            }
12772                            // digit was radix-1: drop it and keep carrying.
12773                        }
12774                    }
12775                }
12776                break;
12777            }
12778            if fraction < delta {
12779                break;
12780            }
12781        }
12782    }
12783
12784    // Integer digits, least-significant first (reversed at the end).
12785    let mut int_out: Vec<u8> = Vec::new();
12786    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12787    while v8_exponent(integer / rf) > 0 {
12788        integer /= rf;
12789        int_out.push(b'0');
12790    }
12791    loop {
12792        let remainder = integer % rf;
12793        int_out.push(digits[remainder as usize]);
12794        integer = (integer - remainder) / rf;
12795        if integer <= 0.0 {
12796            break;
12797        }
12798    }
12799    int_out.reverse();
12800
12801    let mut out: Vec<u8> = Vec::new();
12802    if neg {
12803        out.push(b'-');
12804    }
12805    out.extend_from_slice(&int_out);
12806    if !frac.is_empty() {
12807        out.push(b'.');
12808        out.extend_from_slice(&frac);
12809    }
12810    String::from_utf8(out).unwrap()
12811}
12812
12813/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12814fn next_up(x: f64) -> f64 {
12815    f64::from_bits(x.to_bits() + 1)
12816}
12817
12818/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12819/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12820fn v8_exponent(x: f64) -> i32 {
12821    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12822    if biased == 0 {
12823        -1074 // denormal
12824    } else {
12825        biased - 1075
12826    }
12827}
12828
12829// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12830
12831/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12832/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12833/// but the value kept alongside it is what iteration and `console.log` report,
12834/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12835fn normalize_zero_key(v: Value) -> Value {
12836    match v {
12837        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12838        other => other,
12839    }
12840}
12841
12842fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12843    match name {
12844        "get" => {
12845            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12846            Ok(with_host(|h| match h.get(recv) {
12847                Some(JsObj::Map { entries, .. }) => entries
12848                    .get(&key)
12849                    .map(|(_, v)| v.clone())
12850                    .unwrap_or(Value::Undef),
12851                _ => Value::Undef,
12852            }))
12853        }
12854        "set" => {
12855            let kv = normalize_zero_key(arg0(&args));
12856            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12857            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12858            let key = with_host(|h| host::map_key(h, &kv));
12859            with_host(|h| {
12860                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12861                    entries.insert(key, (kv, vv));
12862                }
12863            });
12864            Ok(recv.clone())
12865        }
12866        "has" => {
12867            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12868            Ok(Value::Bool(with_host(
12869                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12870            )))
12871        }
12872        "delete" => {
12873            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12874            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12875                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12876                _ => false,
12877            })))
12878        }
12879        "clear" => {
12880            with_host(|h| {
12881                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12882                    entries.clear();
12883                }
12884            });
12885            Ok(Value::Undef)
12886        }
12887        "forEach" => {
12888            let cb = arg0(&args);
12889            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12890                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12891                _ => Vec::new(),
12892            });
12893            for (k, v) in pairs {
12894                host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12895            }
12896            Ok(Value::Undef)
12897        }
12898        // LIVE, not a snapshot: an entry added during iteration is visited and
12899        // one deleted before it is reached is not.
12900        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12901            recv,
12902            if name == "@@iterator" {
12903                "entries"
12904            } else {
12905                name
12906            },
12907        )),
12908        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12909    }
12910}
12911
12912/// A weak collection can only hold objects (and unregistered symbols) — a
12913/// primitive key is a `TypeError`, which is how packages probe for weak support.
12914fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12915    let weak = with_host(|h| {
12916        matches!(
12917            h.get(recv),
12918            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12919        )
12920    });
12921    if !weak {
12922        return Ok(());
12923    }
12924    let is_object = with_host(|h| match key {
12925        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12926        _ => false,
12927    });
12928    if is_object {
12929        return Ok(());
12930    }
12931    Err(host::type_error(if kind == "WeakMap" {
12932        "Invalid value used as weak map key"
12933    } else {
12934        "Invalid value used in weak set"
12935    }))
12936}
12937
12938/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12939///
12940/// The seven set operations do NOT require a real `Set` on the right-hand side:
12941/// anything with a numeric `size` and callable `has`/`keys` participates, which
12942/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12943/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12944/// diagnostic, so a bad operand reports which field was wrong rather than
12945/// failing later inside the iteration.
12946struct SetRecord {
12947    obj: Value,
12948    /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12949    /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12950    /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12951    size: f64,
12952    has: Value,
12953    keys: Value,
12954}
12955
12956fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12957    if !with_host(|h| is_object_like(h, other)) {
12958        return Err(host::type_error(&format!(
12959            "Set.prototype.{method} argument must be an object"
12960        )));
12961    }
12962    let raw = get_property(other, "size")?;
12963    let num = host::to_number_value(&raw)?;
12964    if num.is_nan() {
12965        return Err(host::type_error("The .size property is NaN"));
12966    }
12967    let size = num.trunc();
12968    if size < 0.0 {
12969        return Err(host::range_error(&format!("'{size}' is an invalid size")));
12970    }
12971    let has = get_property(other, "has")?;
12972    if !with_host(|h| host::is_callable(h, &has)) {
12973        return Err(host::type_error("string \"has\" is not a function"));
12974    }
12975    let keys = get_property(other, "keys")?;
12976    if !with_host(|h| host::is_callable(h, &keys)) {
12977        return Err(host::type_error("string \"keys\" is not a function"));
12978    }
12979    Ok(SetRecord {
12980        obj: other.clone(),
12981        size,
12982        has,
12983        keys,
12984    })
12985}
12986
12987impl SetRecord {
12988    /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12989    /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12990    fn has(&self, v: &Value) -> Result<bool, String> {
12991        let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12992        Ok(with_host(|h| h.truthy(&r)))
12993    }
12994
12995    /// The operand's elements, drained from the iterator its `keys` method
12996    /// returns. A non-object result is the spec's `Result of the keys method is
12997    /// not an object`, reported before anything is iterated.
12998    fn keys(&self) -> Result<Vec<Value>, String> {
12999        let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
13000        if !with_host(|h| is_object_like(h, &it)) {
13001            return Err(host::type_error(
13002                "Result of the keys method is not an object",
13003            ));
13004        }
13005        host::drain_iterator(&it)
13006    }
13007}
13008
13009/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13010/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13011/// LEFT even though it can on the right.
13012fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13013    if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13014        return Ok(());
13015    }
13016    Err(host::type_error(&format!(
13017        "Method Set.prototype.{method} called on incompatible receiver {}",
13018        with_host(|h| object_tag(h, recv))
13019    )))
13020}
13021
13022/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13023///
13024/// Every one of these operations copies `[[SetData]]` *after* it has touched
13025/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13026/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13027/// result. Snapshotting the receiver up front instead dropped such an element:
13028/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13029fn set_values(recv: &Value) -> Vec<Value> {
13030    with_host(|h| match h.get(recv) {
13031        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13032        _ => Vec::new(),
13033    })
13034}
13035
13036fn set_size(recv: &Value) -> f64 {
13037    with_host(|h| match h.get(recv) {
13038        Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13039        _ => 0.0,
13040    })
13041}
13042
13043/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13044/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13045fn new_set(items: Vec<Value>) -> Result<Value, String> {
13046    let s = with_host(|h| {
13047        h.alloc(JsObj::Set {
13048            entries: IndexMap::new(),
13049            weak: false,
13050        })
13051    });
13052    for v in items {
13053        set_method(&s, "add", vec![v])?;
13054    }
13055    Ok(s)
13056}
13057
13058fn set_contains(s: &Value, v: &Value) -> bool {
13059    let key = with_host(|h| host::map_key(h, v));
13060    with_host(
13061        |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13062    )
13063}
13064
13065/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13066///
13067/// Each one branches on the two sizes and iterates the SMALLER side — not an
13068/// optimization but observable behaviour: which side is walked decides the
13069/// result's order and whether the operand's `has` or its `keys` is the method
13070/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13071/// yields the operand's order, and its `keys` (never its `has`) is called.
13072fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13073    require_set_receiver(recv, name)?;
13074    let other = get_set_record(&arg0(&args), name)?;
13075    let my_size = set_size(recv);
13076    match name {
13077        "union" => {
13078            let keys = other.keys()?;
13079            let mut out = set_values(recv);
13080            out.extend(keys);
13081            new_set(out)
13082        }
13083        "intersection" => {
13084            let mut out = Vec::new();
13085            if my_size <= other.size {
13086                for v in set_values(recv) {
13087                    if other.has(&v)? {
13088                        out.push(v);
13089                    }
13090                }
13091            } else {
13092                for k in other.keys()? {
13093                    if set_contains(recv, &k) {
13094                        out.push(k);
13095                    }
13096                }
13097            }
13098            new_set(out)
13099        }
13100        "difference" => {
13101            if my_size <= other.size {
13102                let mut out = Vec::new();
13103                for v in set_values(recv) {
13104                    if !other.has(&v)? {
13105                        out.push(v);
13106                    }
13107                }
13108                return new_set(out);
13109            }
13110            let out = new_set(set_values(recv))?;
13111            for k in other.keys()? {
13112                set_method(&out, "delete", vec![k])?;
13113            }
13114            Ok(out)
13115        }
13116        "symmetricDifference" => {
13117            // The operand is drained FIRST — the spec takes the iterator before
13118            // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13119            // reflected in the result.
13120            let keys = other.keys()?;
13121            let out = new_set(set_values(recv))?;
13122            for k in keys {
13123                if set_contains(recv, &k) {
13124                    set_method(&out, "delete", vec![k])?;
13125                } else {
13126                    set_method(&out, "add", vec![k])?;
13127                }
13128            }
13129            Ok(out)
13130        }
13131        "isSubsetOf" => {
13132            if my_size > other.size {
13133                return Ok(Value::Bool(false));
13134            }
13135            for v in set_values(recv) {
13136                if !other.has(&v)? {
13137                    return Ok(Value::Bool(false));
13138                }
13139            }
13140            Ok(Value::Bool(true))
13141        }
13142        "isSupersetOf" => {
13143            if my_size < other.size {
13144                return Ok(Value::Bool(false));
13145            }
13146            for k in other.keys()? {
13147                if !set_contains(recv, &k) {
13148                    return Ok(Value::Bool(false));
13149                }
13150            }
13151            Ok(Value::Bool(true))
13152        }
13153        "isDisjointFrom" => {
13154            if my_size <= other.size {
13155                for v in set_values(recv) {
13156                    if other.has(&v)? {
13157                        return Ok(Value::Bool(false));
13158                    }
13159                }
13160            } else {
13161                for k in other.keys()? {
13162                    if set_contains(recv, &k) {
13163                        return Ok(Value::Bool(false));
13164                    }
13165                }
13166            }
13167            Ok(Value::Bool(true))
13168        }
13169        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13170    }
13171}
13172
13173fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13174    match name {
13175        "add" => {
13176            let vv = normalize_zero_key(arg0(&args));
13177            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13178            let key = with_host(|h| host::map_key(h, &vv));
13179            with_host(|h| {
13180                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13181                    entries.insert(key, vv);
13182                }
13183            });
13184            Ok(recv.clone())
13185        }
13186        "has" => {
13187            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13188            Ok(Value::Bool(with_host(
13189                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13190            )))
13191        }
13192        "delete" => {
13193            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13194            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13195                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13196                _ => false,
13197            })))
13198        }
13199        "clear" => {
13200            with_host(|h| {
13201                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13202                    entries.clear();
13203                }
13204            });
13205            Ok(Value::Undef)
13206        }
13207        "forEach" => {
13208            let cb = arg0(&args);
13209            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13210                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13211                _ => Vec::new(),
13212            });
13213            for v in vals {
13214                host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13215            }
13216            Ok(Value::Undef)
13217        }
13218        "union"
13219        | "intersection"
13220        | "difference"
13221        | "symmetricDifference"
13222        | "isSubsetOf"
13223        | "isSupersetOf"
13224        | "isDisjointFrom" => set_operation(recv, name, args),
13225        // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13226        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13227            recv,
13228            if name == "entries" {
13229                "entries"
13230            } else {
13231                "values"
13232            },
13233        )),
13234        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13235    }
13236}
13237
13238fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13239    // A generator IS its own iterator: both symbol forms return the receiver.
13240    if matches!(name, "@@iterator" | "@@asyncIterator") {
13241        return Ok(recv.clone());
13242    }
13243    // An `async function*` object's methods return PROMISES of the record, and
13244    // its body has to be driven through the await-aware stepper (a plain
13245    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13246    if host::is_async_generator(recv) {
13247        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13248        // `.return`/`.throw` must wait behind a `.next()` that is still
13249        // suspended on an internal `await`, or that `.next()` would report
13250        // `{done: true}` for a value the body had not yet reached. An uncaught
13251        // `.throw(e)` rejects the returned promise; it does not throw here.
13252        return match name {
13253            "next" => Ok(host::async_gen_enqueue(
13254                recv,
13255                host::GenReq::Next(arg0(&args)),
13256            )),
13257            "return" => Ok(host::async_gen_enqueue(
13258                recv,
13259                host::GenReq::Return(arg0(&args)),
13260            )),
13261            "throw" => Ok(host::async_gen_enqueue(
13262                recv,
13263                host::GenReq::Throw(arg0(&args)),
13264            )),
13265            "@@asyncIterator" => Ok(recv.clone()),
13266            _ => Err(host::type_error(&format!(
13267                "asyncGenerator.{name} is not a function"
13268            ))),
13269        };
13270    }
13271    match name {
13272        "next" => {
13273            let send = arg0(&args);
13274            match host::gen_resume(recv, send)? {
13275                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13276                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13277            }
13278        }
13279        "return" => {
13280            // Resume with an injected return so any pending `finally` runs; the
13281            // completion may itself be a `finally` yield (not-done) or the value.
13282            match host::gen_return(recv, arg0(&args))? {
13283                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13284                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13285            }
13286        }
13287        "throw" => {
13288            // Inject a throw at the suspension point: an enclosing `try/catch` in
13289            // the body can handle it (and any `finally` runs); otherwise it
13290            // propagates to the caller.
13291            match host::gen_throw(recv, arg0(&args))? {
13292                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13293                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13294            }
13295        }
13296        _ => Err(host::type_error(&format!(
13297            "generator.{name} is not a function"
13298        ))),
13299    }
13300}
13301
13302/// A `{ value, done }` iterator-result object.
13303fn iter_result(value: Value, done: bool) -> Value {
13304    with_host(|h| {
13305        let mut m: IndexMap<String, Value> = IndexMap::new();
13306        m.insert("value".into(), value);
13307        m.insert("done".into(), Value::Bool(done));
13308        h.new_object(m)
13309    })
13310}
13311
13312/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
13313/// cursor over a materialized item list.
13314fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13315    match name {
13316        "next" => {
13317            let step = with_host(|h| {
13318                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13319                    if *idx < items.len() {
13320                        let v = items[*idx].clone();
13321                        *idx += 1;
13322                        return Some(v);
13323                    }
13324                }
13325                None
13326            });
13327            Ok(match step {
13328                Some(v) => iter_result(v, false),
13329                None => iter_result(Value::Undef, true),
13330            })
13331        }
13332        "return" => {
13333            // Exhaust the cursor and report done.
13334            with_host(|h| {
13335                if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13336                    *idx = items.len();
13337                }
13338            });
13339            Ok(iter_result(arg0(&args), true))
13340        }
13341        // An iterator is its own iterable.
13342        "@@iterator" => Ok(recv.clone()),
13343        _ => Err(host::type_error(&format!(
13344            "iterator.{name} is not a function"
13345        ))),
13346    }
13347}
13348
13349fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13350    match name {
13351        "toString" => Ok(with_host(|h| {
13352            let s = h.str_of(recv);
13353            h.new_str(s)
13354        })),
13355        // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13356        // itself for EVERY hint — it ignores its argument. That is what makes
13357        // `sym + ''` a TypeError rather than a concatenation: the conversion
13358        // succeeds and hands back a symbol, and it is `+` that then rejects it.
13359        "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13360        _ => Err(host::type_error(&format!(
13361            "symbol.{name} is not a function"
13362        ))),
13363    }
13364}
13365
13366// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13367
13368fn object_create(args: Vec<Value>) -> Result<Value, String> {
13369    let proto = arg0(&args);
13370    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13371    // `undefined` is NOT accepted — measured on node v26.7.0,
13372    // `Object.create(undefined)` is
13373    // `TypeError: Object prototype may only be an Object or null: undefined`,
13374    // where node-js quietly built a normal object.
13375    reject_bad_prototype(&proto)?;
13376    let obj = with_host(|h| h.new_object(IndexMap::new()));
13377    // `set_proto` records a null proto as an explicit null-prototype object.
13378    with_host(|h| h.set_proto(&obj, proto));
13379    // Optional second arg: a property-descriptor map.
13380    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13381        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13382            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13383            _ => Vec::new(),
13384        });
13385        for (k, d) in entries {
13386            apply_descriptor(&obj, &k, &d)?;
13387        }
13388    }
13389    Ok(obj)
13390}
13391
13392/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13393/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13394/// `EventEmitter.prototype` (the one express mixes onto its app function).
13395/// The own property names of `<Ctor>.prototype`, and whether each is
13396/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13397///
13398/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13399/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13400/// but were not enumerable, so feature detection that walks a prototype found
13401/// nothing there. The table is read from the reference engine rather than
13402/// derived from the arity table because the arity table holds functions only:
13403/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13404/// `URL.prototype` components are accessors.
13405fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13406    let ctor = ns.strip_suffix(".prototype")?;
13407    crate::arity::PROTO_MEMBERS
13408        .binary_search_by(|(k, _)| (*k).cmp(ctor))
13409        .ok()
13410        .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13411}
13412
13413fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13414    match ns {
13415        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13416        _ => None,
13417    }
13418}
13419
13420/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13421/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13422/// filtered out of); every other receiver answers from its property map.
13423fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13424    if let Some(keys) = crate::proxy::own_keys(v)? {
13425        return Ok(keys
13426            .iter()
13427            .filter(|k| host::is_symbol_key(k))
13428            .map(|k| crate::proxy::key_value(k))
13429            .collect());
13430    }
13431    // An intrinsic prototype's symbol-keyed members come from the generated
13432    // table, which is the only record of them: they own no map entry, so
13433    // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13434    // reports `Symbol.iterator` and `Symbol.unscopables`.
13435    if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13436        if let Some(members) = intrinsic_proto_members(&ns) {
13437            return Ok(with_host(|h| {
13438                members
13439                    .iter()
13440                    .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13441                    .map(|name| h.well_known_symbol(name))
13442                    .collect()
13443            }));
13444        }
13445    }
13446    Ok(with_host(|h| h.own_symbol_keys(v)))
13447}
13448
13449/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13450pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13451    object_define_property(vec![obj.clone(), key, desc])
13452}
13453
13454/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13455pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13456    object_get_own_descriptor(vec![obj.clone(), key])
13457}
13458
13459fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13460    let obj = arg0(&args);
13461    // A Proxy defines through its `defineProperty` trap; the target it forwards
13462    // to is where the ordinary path below finally runs.
13463    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13464        let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13465        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13466        if !with_host(|h| is_object_like(h, &desc)) {
13467            return Err(host::type_error(&format!(
13468                "Property description must be an object: {}",
13469                with_host(|h| h.str_of(&desc))
13470            )));
13471        }
13472        // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13473        // too. `Reflect.defineProperty` is the form that reports `false`.
13474        if !crate::proxy::define_property(&obj, &key, &desc)? {
13475            return Err(host::type_error(&format!(
13476                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13477            )));
13478        }
13479        return Ok(obj);
13480    }
13481    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13482    // target and a non-object descriptor each throw before anything is written.
13483    if !with_host(|h| is_object_like(h, &obj)) {
13484        return Err(host::type_error(
13485            "Object.defineProperty called on non-object",
13486        ));
13487    }
13488    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13489    if !with_host(|h| is_object_like(h, &desc)) {
13490        return Err(host::type_error(&format!(
13491            "Property description must be an object: {}",
13492            with_host(|h| h.str_of(&desc))
13493        )));
13494    }
13495    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13496    apply_descriptor(&obj, &key, &desc)?;
13497    Ok(obj)
13498}
13499
13500/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13501/// for anything else (28.1). A primitive was being accepted and silently
13502/// producing nothing.
13503/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13504/// `Reflect.construct` take.
13505///
13506/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13507/// iterator was being used instead, so an array-like produced nothing and a
13508/// primitive produced nothing rather than the TypeError node raises.
13509/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13510/// object still accepts, because it changes nothing.
13511///
13512/// The observable prototype, not the stored link: an ordinary object has no
13513/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13514/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13515/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13516/// step 8 walks up from `p` looking for `obj`.
13517///
13518/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13519/// built a ring. Nothing hung, because every chain walk in this host carries a
13520/// hop limit, but a lookup then silently gave up instead of finding a property
13521/// that really was there.
13522fn would_cycle(obj: &Value, p: &Value) -> bool {
13523    let mut cur = Some(p.clone());
13524    for _ in 0..1000 {
13525        let Some(c) = cur else { return false };
13526        if with_host(|h| h.strict_eq(&c, obj)) {
13527            return true;
13528        }
13529        // A PROXY's prototype is its handler's business; the spec skips the
13530        // walk entirely when one is in the chain.
13531        if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13532            return false;
13533        }
13534        cur = with_host(|h| h.proto_of(&c));
13535    }
13536    false
13537}
13538
13539fn same_prototype(obj: &Value, p: &Value) -> bool {
13540    let cur = prototype_of(obj);
13541    with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13542}
13543
13544fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13545    if !with_host(|h| is_object_like(h, v)) {
13546        return Err(host::type_error(
13547            "CreateListFromArrayLike called on non-object",
13548        ));
13549    }
13550    let len = get_property(v, "length")?;
13551    let n = with_host(|h| h.to_number(&len));
13552    let n = if n.is_finite() && n > 0.0 {
13553        n as usize
13554    } else {
13555        0
13556    };
13557    (0..n).map(|i| get_property(v, &i.to_string())).collect()
13558}
13559
13560fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13561    if with_host(|h| is_object_like(h, v)) {
13562        return Ok(());
13563    }
13564    Err(host::type_error(&format!(
13565        "Reflect.{method} called on non-object"
13566    )))
13567}
13568
13569/// Whether `v` is an Object in the language sense — anything `typeof` calls
13570/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13571/// distinguish "an object" from a primitive.
13572fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13573    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13574}
13575
13576/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13577/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13578/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13579/// `assign` family: each returned an empty result for `null` where node v26.7.0
13580/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13581/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13582fn require_object_coercible(v: &Value) -> Result<(), String> {
13583    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13584        return Err(host::type_error(
13585            "Cannot convert undefined or null to object",
13586        ));
13587    }
13588    Ok(())
13589}
13590
13591/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13592/// nor `null`, with V8's wording. Measured on node v26.7.0:
13593/// `Object.create("s")` is
13594/// `TypeError: Object prototype may only be an Object or null: s`.
13595fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13596    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13597        return Ok(());
13598    }
13599    Err(host::type_error(&format!(
13600        "Object prototype may only be an Object or null: {}",
13601        with_host(|h| h.str_of(proto))
13602    )))
13603}
13604
13605/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13606///
13607/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13608/// `configurable` field defaults to **false** — which is why a `defineProperty`
13609/// data property is invisible to `Object.keys` unless the caller opts in. That
13610/// asymmetry against plain assignment is the whole reason the attribute table
13611/// exists.
13612/// The requested fields of a property descriptor — 10.1.6.2
13613/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13614/// is the distinction the merge below turns on: an omitted field LEAVES an
13615/// existing attribute alone rather than resetting it.
13616struct Requested {
13617    value: Option<Value>,
13618    get: Option<Option<Value>>,
13619    set: Option<Option<Value>>,
13620    writable: Option<bool>,
13621    enumerable: Option<bool>,
13622    configurable: Option<bool>,
13623}
13624
13625impl Requested {
13626    /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13627    /// `HasProperty`/`Get` pairs do — a descriptor built with
13628    /// `Object.create({ value: 1 })` is legal.
13629    fn read(desc: &Value) -> Self {
13630        let has = |k: &str| {
13631            with_host(|h| {
13632                host::lookup_chain(h, desc, k).is_some()
13633                    || host::lookup_accessor(h, desc, k).is_some()
13634            })
13635        };
13636        let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13637        // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13638        // doing it inside the `with_host` closure aborts on the double borrow.
13639        let flag = |k: &str| {
13640            has(k).then(|| {
13641                let v = val(k);
13642                with_host(|h| h.truthy(&v))
13643            })
13644        };
13645        Requested {
13646            value: has("value").then(|| val("value")),
13647            get: has("get").then(|| match val("get") {
13648                Value::Undef => None,
13649                g => Some(g),
13650            }),
13651            set: has("set").then(|| match val("set") {
13652                Value::Undef => None,
13653                st => Some(st),
13654            }),
13655            writable: flag("writable"),
13656            enumerable: flag("enumerable"),
13657            configurable: flag("configurable"),
13658        }
13659    }
13660
13661    fn is_accessor(&self) -> bool {
13662        self.get.is_some() || self.set.is_some()
13663    }
13664
13665    fn is_data(&self) -> bool {
13666        self.value.is_some() || self.writable.is_some()
13667    }
13668}
13669
13670/// The own property already at `key`, if any, read back through
13671/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13672/// fn-prop side table, Buffer bytes) is covered by one code path.
13673struct Existing {
13674    accessor: bool,
13675    value: Value,
13676    get: Option<Value>,
13677    set: Option<Value>,
13678    writable: bool,
13679    enumerable: bool,
13680    configurable: bool,
13681}
13682
13683fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13684    let k = with_host(|h| h.new_str(key.to_string()));
13685    let d = own_descriptor_pub(obj, k).ok()?;
13686    if matches!(d, Value::Undef) {
13687        return None;
13688    }
13689    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13690    let truthy = |n: &str| {
13691        let v = field(n);
13692        with_host(|h| h.truthy(&v))
13693    };
13694    let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13695    Some(Existing {
13696        accessor,
13697        value: field("value"),
13698        get: match field("get") {
13699            Value::Undef => None,
13700            g => Some(g),
13701        },
13702        set: match field("set") {
13703            Value::Undef => None,
13704            st => Some(st),
13705        },
13706        writable: truthy("writable"),
13707        enumerable: truthy("enumerable"),
13708        configurable: truthy("configurable"),
13709    })
13710}
13711
13712/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13713/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13714/// one with this, not with strict equality.
13715pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13716    let num = |v: &Value| match v {
13717        Value::Int(n) => Some(*n as f64),
13718        Value::Float(f) => Some(*f),
13719        _ => None,
13720    };
13721    match (num(a), num(b)) {
13722        (Some(x), Some(y)) => {
13723            if x.is_nan() && y.is_nan() {
13724                true
13725            } else if x == 0.0 && y == 0.0 {
13726                x.is_sign_negative() == y.is_sign_negative()
13727            } else {
13728                x == y
13729            }
13730        }
13731        _ => with_host(|h| h.strict_eq(a, b)),
13732    }
13733}
13734
13735/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13736///
13737/// None of the validation existed: every `Object.defineProperty` was applied
13738/// unconditionally, so redefining a non-configurable property silently
13739/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13740/// `false` rather than "leave alone", so the ordinary
13741/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13742/// `writable` and `configurable` from a property that had both.
13743///
13744/// Converting an accessor to a data property did not take effect at all: the
13745/// value was written but the accessor stayed in its side table, and accessors
13746/// win on read, so the getter kept answering.
13747fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13748    let req = Requested::read(desc);
13749    let cur = existing_property(obj, key);
13750
13751    // An array's `length` is the exotic own property whose write resizes the
13752    // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13753    // key and left the elements untouched.
13754    if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13755        if let Some(v) = req.value.clone() {
13756            return set_property_pub(obj, "length", v);
13757        }
13758    }
13759
13760    // The other exotics whose own properties are SYNTHESIZED rather than stored
13761    // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13762    // The ordinary path below writes a shadowing map entry the read never
13763    // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13764    // unchanged.
13765    // A builtin namespace/prototype has no property map either, so a data
13766    // descriptor has to reach the same side table an assignment does.
13767    // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13768    // careful polyfill installs itself, precisely to avoid the enumerable
13769    // property a bare assignment creates — wrote a map entry nothing read.
13770    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13771        if let Some(v) = req.value.clone() {
13772            return set_property_pub(obj, key, v);
13773        }
13774    }
13775    let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13776        && key.parse::<usize>().is_ok())
13777        || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13778    if exotic_own {
13779        if let Some(v) = req.value.clone() {
13780            return set_property_pub(obj, key, v);
13781        }
13782    }
13783
13784    // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13785    // object. Only an existing property's attributes were being validated, so
13786    // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13787    if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13788        return Err(host::type_error(&format!(
13789            "Cannot define property {key}, object is not extensible"
13790        )));
13791    }
13792    if let Some(c) = &cur {
13793        if !c.configurable {
13794            let rejected = req.configurable == Some(true)
13795                || req.enumerable.is_some_and(|e| e != c.enumerable)
13796                || (req.is_accessor() && !c.accessor)
13797                || (req.is_data() && c.accessor)
13798                || (c.accessor
13799                    && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13800                        || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13801                || (!c.accessor
13802                    && !c.writable
13803                    && (req.writable == Some(true)
13804                        || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13805            if rejected {
13806                return Err(host::type_error(&format!(
13807                    "Cannot redefine property: {key}"
13808                )));
13809            }
13810        }
13811    }
13812
13813    // An omitted field keeps what the property already had; a brand-new
13814    // property defaults every one of them to false.
13815    let attrs = host::PropAttrs {
13816        writable: req
13817            .writable
13818            .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13819        enumerable: req
13820            .enumerable
13821            .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13822        configurable: req
13823            .configurable
13824            .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13825    };
13826    with_host(|h| h.set_prop_attrs(obj, key, attrs));
13827
13828    if req.is_accessor() {
13829        let get = req
13830            .get
13831            .clone()
13832            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13833        let set = req
13834            .set
13835            .clone()
13836            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13837        // An ACCESSOR at an index past the end still extends the array
13838        // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13839        // `length === 5` with holes between. Only the DATA path grew it, so
13840        // the accessor landed in the side table while `length` stayed put —
13841        // and with it out of range, `Object.keys` and `JSON.stringify` never
13842        // saw the index at all.
13843        if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13844        {
13845            with_host(|h| {
13846                let old_len = match h.get(obj) {
13847                    Some(JsObj::Array(items)) => items.len(),
13848                    _ => 0,
13849                };
13850                if i >= old_len {
13851                    if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13852                        items.resize(i + 1, Value::Undef);
13853                    }
13854                    h.mark_hole_range(obj, old_len..i + 1);
13855                }
13856            });
13857        }
13858        with_host(|h| h.set_accessor(obj, key, get, set));
13859        return Ok(());
13860    }
13861
13862    if let Some(c) = &cur {
13863        if c.accessor {
13864            if !req.is_data() {
13865                // A generic descriptor — flags only — leaves an accessor an
13866                // accessor. They were already applied above.
13867                return Ok(());
13868            }
13869            let v = req.value.clone().unwrap_or(Value::Undef);
13870            with_host(|h| h.accessor_to_data(obj, key, v));
13871            return Ok(());
13872        }
13873    }
13874
13875    let Some(v) = req.value else {
13876        // Nothing to write: a flags-only redefinition of a data property.
13877        return Ok(());
13878    };
13879    write_data_slot(obj, key, v);
13880    Ok(())
13881}
13882
13883/// Store `v` as an own data property, in whichever slot the object kind keeps
13884/// its own properties.
13885fn write_data_slot(obj: &Value, key: &str, v: Value) {
13886    // A function/class receiver stores its own props in the fn-prop side table
13887    // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13888    if matches!(
13889        with_host(|h| h.get(obj).cloned()),
13890        Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13891    ) || uses_side_table(obj)
13892    {
13893        with_host(|h| h.set_fn_prop(obj, key, v));
13894        return;
13895    }
13896    if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13897        // An array's index keys ARE its elements, and defining one past the end
13898        // grows the array with holes in between (10.4.2.1). This whole branch
13899        // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13900        // into the ordinary property map an array does not have, so it was a
13901        // silent no-op.
13902        with_host(|h| {
13903            let old = match h.get(obj) {
13904                Some(JsObj::Array(items)) => items.len(),
13905                _ => 0,
13906            };
13907            if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13908                if i >= old {
13909                    items.resize(i + 1, Value::Undef);
13910                }
13911                items[i] = v;
13912            }
13913            if i > old {
13914                h.mark_hole_range(obj, old..i);
13915            }
13916            h.clear_hole(obj, i);
13917        });
13918        return;
13919    }
13920    with_host(|h| {
13921        if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13922            p.insert(key.to_string(), v);
13923            host::canonicalize_own_keys(p);
13924        }
13925    });
13926}
13927
13928/// `Object.defineProperties(obj, descriptorMap)`.
13929fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
13930    let obj = arg0(&args);
13931    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
13932    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
13933        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13934        _ => Vec::new(),
13935    });
13936    for (k, d) in entries {
13937        apply_descriptor(&obj, &k, &d)?;
13938    }
13939    Ok(obj)
13940}
13941
13942/// The descriptor of an own property a function, a typed array or a RegExp
13943/// SYNTHESIZES rather than keeping in a property map.
13944///
13945/// These read back through the ordinary path but owned no descriptor and did
13946/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
13947/// views of "does this property exist" disagreed — a read said yes while
13948/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
13949/// what a shim checks before patching.
13950fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
13951    let ro_configurable = host::PropAttrs {
13952        writable: false,
13953        enumerable: false,
13954        configurable: true,
13955    };
13956    // A callable's `length`/`name` are read-only but configurable; its
13957    // `prototype` is writable and NOT configurable, and a class's is neither.
13958    // An arrow, a method and a bound function own no `prototype` at all.
13959    if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
13960        return None;
13961    }
13962    if with_host(|h| host::is_callable(h, obj)) {
13963        if key == "prototype" {
13964            let p = get_property(obj, "prototype").ok()?;
13965            if matches!(p, Value::Undef) {
13966                return None;
13967            }
13968            return Some((
13969                p,
13970                host::PropAttrs {
13971                    writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
13972                    enumerable: false,
13973                    configurable: false,
13974                },
13975            ));
13976        }
13977        return Some((get_property(obj, key).ok()?, ro_configurable));
13978    }
13979    // A typed array's elements are own, enumerable, writable, configurable
13980    // properties; an index past the end owns nothing.
13981    if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
13982        let v = crate::stdlib::typedarray::elem_get(obj, key)?;
13983        return Some((
13984            v,
13985            host::PropAttrs {
13986                writable: true,
13987                enumerable: true,
13988                configurable: true,
13989            },
13990        ));
13991    }
13992    // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
13993    if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
13994        return Some((
13995            get_property(obj, "lastIndex").ok()?,
13996            host::PropAttrs {
13997                writable: true,
13998                enumerable: false,
13999                configurable: false,
14000            },
14001        ));
14002    }
14003    None
14004}
14005
14006fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14007    let obj = arg0(&args);
14008    require_object_coercible(&obj)?;
14009    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14010    // A string primitive's boxed own properties: each code-unit index is an
14011    // enumerable, non-writable, non-configurable data property, and `length` is
14012    // the same minus enumerable.
14013    if let Some(units) = string_primitive_units(&obj) {
14014        let entry = match key.parse::<usize>() {
14015            Ok(i) => units
14016                .get(i)
14017                .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14018            Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14019            Err(_) => None,
14020        };
14021        return Ok(match entry {
14022            Some((value, enumerable)) => with_host(|h| {
14023                let mut m: IndexMap<String, Value> = IndexMap::new();
14024                m.insert("value".into(), value);
14025                m.insert("writable".into(), Value::Bool(false));
14026                m.insert("enumerable".into(), Value::Bool(enumerable));
14027                m.insert("configurable".into(), Value::Bool(false));
14028                h.new_object(m)
14029            }),
14030            None => Value::Undef,
14031        });
14032    }
14033    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14034        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14035    }
14036    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14037    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14038    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14039        if let Some(names) = builtin_proto_method_names(&ns) {
14040            if names.contains(&key.as_str()) {
14041                return Ok(with_host(|h| {
14042                    let thunk = h.alloc(JsObj::Builtin(format!(
14043                        "@proto:{}:{key}",
14044                        ns.trim_end_matches(".prototype")
14045                    )));
14046                    let mut m: IndexMap<String, Value> = IndexMap::new();
14047                    m.insert("value".into(), thunk);
14048                    m.insert("writable".into(), Value::Bool(true));
14049                    m.insert("enumerable".into(), Value::Bool(true));
14050                    m.insert("configurable".into(), Value::Bool(true));
14051                    h.new_object(m)
14052                }));
14053            }
14054        }
14055    }
14056    // A global the object does not own outright is still an own property of the
14057    // global object — the same lazy binding the bare identifier resolves to.
14058    // Every one of them reported `undefined`, so a feature probe written as
14059    // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14060    // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14061    // rest are ordinary writable, non-enumerable, configurable bindings.
14062    if with_host(|h| h.is_global_object(&obj)) {
14063        let owned = with_host(|h| match h.get(&obj) {
14064            Some(JsObj::Object(p)) => p.contains_key(&key),
14065            _ => false,
14066        });
14067        if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14068            // A global a SCRIPT created — `x = 1` with no declaration — is an
14069            // ordinary enumerable property, unlike the builtins.
14070            let script_made = with_host(|h| h.read_global(&key).is_some());
14071            if let Some(v) = global_object_binding(&key) {
14072                let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14073                return Ok(with_host(|h| {
14074                    let mut m: IndexMap<String, Value> = IndexMap::new();
14075                    m.insert("value".into(), v);
14076                    m.insert("writable".into(), Value::Bool(!frozen));
14077                    m.insert(
14078                        "enumerable".into(),
14079                        Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14080                    );
14081                    m.insert("configurable".into(), Value::Bool(!frozen));
14082                    h.new_object(m)
14083                }));
14084            }
14085        }
14086    }
14087    // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14088    // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14089    // one of these reads back a value, but none owned a DESCRIPTOR:
14090    // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14091    // as "no such property" to the shim/polyfill family that probes a namespace
14092    // before patching it.
14093    // An ACCESSOR member describes itself with a `get`, never a `value` — and
14094    // it must do so without READING the property, since running the getter
14095    // against the prototype is exactly what throws. Both prototype
14096    // representations are covered, so `Symbol.prototype.description` and
14097    // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14098    // "no such property" to anything that probes before patching.
14099    if let Some(ctor) = intrinsic_proto_of(&obj) {
14100        if is_proto_accessor(&ctor, &key) {
14101            let getter = proto_getter(&ctor, &key);
14102            // The poison pair is the only ECMAScript accessor here with a
14103            // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14104            // `hostname` and the rest are all writable, and reporting them as
14105            // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14106            // 'href').set` read `undefined` for a setter that runs.
14107            let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14108                || crate::stdlib::instance_accessors(&ctor)
14109                    .0
14110                    .iter()
14111                    .any(|(k, settable)| *k == key && *settable);
14112            let setter = writable
14113                .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14114            return Ok(with_host(|h| {
14115                let mut m: IndexMap<String, Value> = IndexMap::new();
14116                m.insert("get".into(), getter);
14117                // `undefined`, not null: a read-only accessor has no setter at
14118                // all, and `JSON.stringify` of the descriptor must drop the key
14119                // rather than report `"set": null`.
14120                m.insert("set".into(), setter.unwrap_or(Value::Undef));
14121                m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14122                m.insert("configurable".into(), Value::Bool(true));
14123                h.new_object(m)
14124            }));
14125        }
14126    }
14127    if let Some(ns) = with_host(|h| match h.get(&obj) {
14128        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14129        _ => None,
14130    }) {
14131        let value = namespace_property(&ns, &key);
14132        if !matches!(value, Value::Undef) {
14133            return Ok(builtin_member_descriptor(&ns, &key, value));
14134        }
14135    }
14136    if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14137        return Ok(with_host(|h| {
14138            let mut m: IndexMap<String, Value> = IndexMap::new();
14139            m.insert("value".into(), value);
14140            m.insert("writable".into(), Value::Bool(attrs.writable));
14141            m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14142            m.insert("configurable".into(), Value::Bool(attrs.configurable));
14143            h.new_object(m)
14144        }));
14145    }
14146    // Accessor descriptor?
14147    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14148        return Ok(with_host(|h| {
14149            let a = h.prop_attrs(&obj, &key);
14150            let mut m: IndexMap<String, Value> = IndexMap::new();
14151            m.insert("get".into(), get.unwrap_or(Value::Undef));
14152            m.insert("set".into(), set.unwrap_or(Value::Undef));
14153            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14154            m.insert("configurable".into(), Value::Bool(a.configurable));
14155            h.new_object(m)
14156        }));
14157    }
14158    let val = with_host(|h| match h.get(&obj) {
14159        // A Buffer's own properties are exactly its byte indices, read out of the
14160        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14161        // that V8 keeps on the prototype, so they own no descriptor.
14162        Some(JsObj::Object(p))
14163            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14164        {
14165            match (
14166                p.get("@@bytes").and_then(|b| h.get(b)),
14167                key.parse::<usize>(),
14168            ) {
14169                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14170                _ => None,
14171            }
14172        }
14173        Some(JsObj::Object(p)) => p.get(&key).cloned(),
14174        // An array's index keys read the elements; `length` is the exotic own
14175        // property; anything else is an ordinary own key in the side table.
14176        Some(JsObj::Array(items)) => match key.parse::<usize>() {
14177            // An ELIDED index owns no property at all, so it has no descriptor.
14178            Ok(i) if h.is_hole(&obj, i) => None,
14179            Ok(i) => items.get(i).cloned(),
14180            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14181            Err(_) => h.fn_prop(&obj, &key),
14182        },
14183        // A function/class own prop lives in the fn-prop side table.
14184        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14185        _ => None,
14186    });
14187    match val {
14188        Some(v) => Ok(with_host(|h| {
14189            let a = h.prop_attrs(&obj, &key);
14190            let mut m: IndexMap<String, Value> = IndexMap::new();
14191            m.insert("value".into(), v);
14192            m.insert("writable".into(), Value::Bool(a.writable));
14193            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14194            m.insert("configurable".into(), Value::Bool(a.configurable));
14195            h.new_object(m)
14196        })),
14197        None => Ok(Value::Undef),
14198    }
14199}
14200
14201/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14202/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14203/// the standard "clone with accessors intact" idiom, so this must agree
14204/// key-for-key with `getOwnPropertyNames`.
14205fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14206    let obj = arg0(&args);
14207    let names = object_keys(vec![obj.clone()], 3)?;
14208    let keys: Vec<String> = with_host(|h| match h.get(&names) {
14209        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14210        _ => Vec::new(),
14211    });
14212    let mut out: IndexMap<String, Value> = IndexMap::new();
14213    for k in keys {
14214        let ks = with_host(|h| h.new_str(k.clone()));
14215        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14216        if !matches!(d, Value::Undef) {
14217            out.insert(k, d);
14218        }
14219    }
14220    Ok(with_host(|h| h.new_object(out)))
14221}
14222
14223/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14224/// Proxy's `has` trap is user code and may throw.
14225pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14226    if let Some(b) = crate::proxy::has(obj, key)? {
14227        return Ok(b);
14228    }
14229    Ok(has_property_ordinary(obj, key))
14230}
14231
14232/// `[[HasProperty]]` for every non-Proxy receiver.
14233fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14234    // `key in globalThis`: membership matches what the READ answers, which for
14235    // the global object includes every lazily-bound builtin and every global a
14236    // script created. `'Math' in globalThis` and `'x' in globalThis` after
14237    // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14238    // read back fine.
14239    if with_host(|h| h.is_global_object(obj))
14240        && !CJS_WRAPPER_LOCALS.contains(&key)
14241        && global_object_binding(key).is_some()
14242    {
14243        return true;
14244    }
14245    // `key in <builtin namespace/prototype>`: membership matches what a property
14246    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14247    // prototype methods) resolve as callable thunks via `namespace_property`, so
14248    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14249    // with the `in` operator before reading the intrinsic).
14250    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14251        return !matches!(namespace_property(&ns, key), Value::Undef);
14252    }
14253    // An integer index of a typed array / Buffer is an own property, and lives
14254    // in the hidden element array rather than the property map — the same
14255    // question `hasOwnProperty` answers, through the same helper. Only a hit
14256    // short-circuits: a non-index key like `'length'` must still fall through
14257    // to the ordinary chain lookup below.
14258    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14259        return true;
14260    }
14261    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14262        return true;
14263    }
14264    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14265        return true;
14266    }
14267    // A member patched onto the receiver's intrinsic prototype. The READ
14268    // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14269    // function while `'at' in []` stayed false.
14270    if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14271        return true;
14272    }
14273    if with_host(|h| match h.get(obj) {
14274        Some(JsObj::Object(p)) => p.contains_key(key),
14275        Some(JsObj::Array(items)) => {
14276            key == "length"
14277                || key
14278                    .parse::<usize>()
14279                    .map(|i| i < items.len() && !h.is_hole(obj, i))
14280                    .unwrap_or(false)
14281                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14282                // side table, and `in` must see it.
14283                || h.fn_prop(obj, key).is_some()
14284        }
14285        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14286        // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14287        // the `RegExpObj` struct rather than a property map, so nothing above
14288        // can see it.
14289        Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14290        _ => false,
14291    }) {
14292        return true;
14293    }
14294    // An INHERITED builtin prototype method. These are not objects on the
14295    // prototype chain — they are synthesized by the read path from the
14296    // intrinsic table — so neither `lookup_chain` nor the property map above
14297    // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14298    // Promise.resolve()` all answered false. That last one is the standard
14299    // thenable test, so the `in` operator disagreed with what a read gives for
14300    // every builtin method of every builtin kind.
14301    inherited_builtin_method(obj, key)
14302}
14303
14304/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14305/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14306/// performs no read, so a getter cannot fire.
14307/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14308/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14309/// `None`.
14310///
14311/// The intrinsic prototypes are namespace handles rather than real objects on
14312/// the chain, so an assignment onto one lands in `builtin_statics` and no
14313/// ordinary chain walk can see it. This is the read side: the receiver's own
14314/// constructor's prototype first, then `Object.prototype`, mirroring
14315/// `inherited_method_owner`'s two-step.
14316pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14317    if with_host(|h| h.has_null_proto(obj)) {
14318        return None;
14319    }
14320    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14321        Some(c) => Some(c),
14322        None if is_arguments(obj) => Some("Object"),
14323        None => with_host(|h| default_ctor_name(h, obj)),
14324    };
14325    // Only the side table is consulted, never the real prototype OBJECT's map:
14326    // `String.prototype` and friends are materialized with their intrinsic
14327    // members present, so reading their maps here would re-route every ordinary
14328    // `"a".toString()` through this path — which recursed until the stack blew.
14329    // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14330    // table precisely so the read side can stay this narrow.
14331    let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14332    let found = ctor.and_then(on).or_else(|| on("Object"))?;
14333    // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14334    // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14335    // name back into the table. Dispatching to it would re-enter this lookup
14336    // and recurse until the stack blew, so a thunk that is already this key's
14337    // own intrinsic reports nothing and the ordinary builtin path answers.
14338    let self_thunk = with_host(
14339        |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14340    );
14341    (!self_thunk).then_some(found)
14342}
14343
14344/// Whether `recv` carries `key` as an OWN property — the guard on
14345/// [`inherited_builtin_static`], since an own property shadows anything
14346/// patched onto a prototype.
14347fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14348    with_host(|h| {
14349        if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14350            return true;
14351        }
14352        match h.get(recv) {
14353            Some(JsObj::Object(p)) => p.contains_key(key),
14354            // An ELIDED index owns nothing — the whole point of a hole is that
14355            // the lookup continues up the chain — so it must not count as a
14356            // shadow here or an inherited value at that index stays invisible.
14357            Some(JsObj::Array(items)) => {
14358                key == "length" || {
14359                    key.parse::<usize>()
14360                        .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14361                }
14362            }
14363            _ => false,
14364        }
14365    })
14366}
14367
14368fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14369    if with_host(|h| h.has_null_proto(obj)) {
14370        return false;
14371    }
14372    if let Some(tag) = crate::stdlib::native_tag(obj) {
14373        if crate::stdlib::instance_has_method(&tag, key) {
14374            return true;
14375        }
14376    }
14377    inherited_method_owner(obj, key).is_some()
14378}
14379
14380/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14381/// whether `Array.prototype`'s methods are still reachable from an array.
14382///
14383/// A builtin's methods are synthesized from the receiver's KIND rather than
14384/// found on a chain, so replacing the prototype could not take them away:
14385/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14386/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14387/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14388/// all still answer, as they do in node.
14389///
14390/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14391/// link at all, so this answers true after one map probe and allocates nothing.
14392pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14393    own_intrinsic_reachable(recv)
14394}
14395
14396fn own_intrinsic_reachable(recv: &Value) -> bool {
14397    // A BOXED primitive needs no special case here: its methods resolve through
14398    // `inherited_method_owner`, which applies the wrapper rule itself.
14399    with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14400}
14401
14402/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14403fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14404    let own = Some(ctor);
14405    let mut cur = recv.clone();
14406    for _ in 0..100 {
14407        let explicit = with_host(|h| h.proto_of(&cur));
14408        let Some(p) = explicit else {
14409            // No explicit link: the implicit prototype is this object's own
14410            // kind's, which is what `recv` is asking about only while `cur` is
14411            // still `recv` itself.
14412            if with_host(|h| h.has_null_proto(&cur)) {
14413                return false;
14414            }
14415            let implicit = with_host(|h| default_ctor_name(h, &cur));
14416            // Every implicit prototype chain ends at `Object.prototype`, so a
14417            // question about `Object` is answered yes by any of them.
14418            return implicit == own || ctor == "Object";
14419        };
14420        if with_host(|h| h.is_null(&p)) {
14421            return false;
14422        }
14423        let hit = with_host(|h| {
14424            own.is_some_and(|c| {
14425                matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14426                    || h.intrinsic_proto_ctor(&p) == Some(c)
14427                    || (c == "Object" && h.object_proto() == p)
14428            })
14429        });
14430        if hit {
14431            return true;
14432        }
14433        // A CLASS prototype object is not linked to the builtin its class
14434        // extends — the `extends` relationship is recorded on the class value —
14435        // so the walk has to cross over there or `class D extends Array {}` ends
14436        // it, and every inherited method of every subclass instance vanishes.
14437        if let Some(builtin) = with_host(|h| {
14438            h.class_owning_proto(&p)
14439                .and_then(|c| h.class_builtin_ancestor(&c))
14440                .map(|b| h.callable_name(&b))
14441        }) {
14442            if own == Some(builtin.as_str()) || ctor == "Object" {
14443                return true;
14444            }
14445        }
14446        cur = p;
14447    }
14448    false
14449}
14450
14451/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14452/// — the complement of [`intrinsic_reachable`], which asks about one known
14453/// constructor.
14454///
14455/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14456/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14457/// it: `o.push(1)` works, because those methods are generic over their receiver
14458/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14459/// worked here). Deciding the owner from the receiver's KIND alone made every
14460/// one of them `undefined` — the same "methods come from the kind, not the
14461/// chain" mistake as the detachment case, in the opposite direction.
14462pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14463    chain_intrinsic_ctors(recv)
14464}
14465
14466fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14467    with_host(|h| chain_intrinsic_ctors_h(h, recv))
14468}
14469
14470/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14471/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14472/// back through `with_host` there aborts the process on a double borrow.
14473pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14474    let mut out: Vec<&'static str> = Vec::new();
14475    let mut cur = recv.clone();
14476    for _ in 0..100 {
14477        let Some(p) = h.proto_of(&cur) else {
14478            break;
14479        };
14480        if h.is_null(&p) {
14481            break;
14482        }
14483        let name = match h.get(&p) {
14484            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14485            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14486        };
14487        if let Some(n) = name {
14488            if let Some(c) = crate::arity::PROTO_MEMBERS
14489                .iter()
14490                .map(|(k, _)| *k)
14491                .find(|k| *k == n)
14492            {
14493                if !out.contains(&c) {
14494                    out.push(c);
14495                }
14496            }
14497        }
14498        cur = p;
14499    }
14500    out
14501}
14502
14503/// The constructor whose prototype defines `key` for `obj` — its own if that
14504/// prototype has it, otherwise `Object` — or `None` when neither does.
14505///
14506/// Used both by `in` and by the READ, so the two cannot disagree about which
14507/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14508/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14509pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14510    inherited_method_owner(obj, key)
14511}
14512
14513fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14514    if with_host(|h| h.has_null_proto(obj)) {
14515        return None;
14516    }
14517    // The generated prototype-member table, which unlike the arity table knows
14518    // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14519    // on a Symbol are members but not functions — and about `constructor`.
14520    // A BOXED primitive reports its wrapper's constructor, not `Object` —
14521    // `'description' in Object(Symbol())` is true. The box is an ordinary
14522    // object carrying the primitive in a slot, so the ctor comes from what it
14523    // holds rather than from the box itself.
14524    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14525        Some(c) => Some(c),
14526        // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14527        // spread and `for-of` work, but node's is an exotic that inherits from
14528        // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14529        // its backing kind would hand it the whole `Array.prototype`.
14530        None if is_arguments(obj) => Some("Object"),
14531        None => with_host(|h| default_ctor_name(h, obj)),
14532    };
14533    let on_proto = |c: &str| {
14534        crate::arity::PROTO_MEMBERS
14535            .binary_search_by(|(k, _)| (*k).cmp(c))
14536            .ok()
14537            .is_some_and(|i| {
14538                crate::arity::PROTO_MEMBERS[i]
14539                    .1
14540                    .iter()
14541                    .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14542            })
14543    };
14544    // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14545    // well-known symbol member is absent from it. For an object whose CHAIN
14546    // reaches an intrinsic prototype the intrinsic table has to be consulted as
14547    // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14548    // at all. It is deliberately NOT consulted for the receiver's own kind:
14549    // there a thunk would be minted for every `@@` member the table names,
14550    // including ones whose dispatch has no implementation for that receiver,
14551    // and `[...buffer]` then failed with `@@iterator is not a function`.
14552    //
14553    // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14554    // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14555    // and minting a thunk there produced `@@iterator is not a function` for
14556    // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14557    // fast path, which the table entry would shadow.
14558    let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14559        && crate::stdlib::native_tag(obj).is_none();
14560    let on_proto_or_symbol =
14561        |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14562    // Each candidate is only an answer while ITS prototype is still on the
14563    // receiver's chain. The two are asked separately: replacing an array's
14564    // prototype with a plain object takes `Array.prototype`'s methods away and
14565    // leaves `Object.prototype`'s, since the replacement inherits from it.
14566    if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14567        return Some(c);
14568    }
14569    // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14570    // kind does not account for.
14571    if let Some(c) = chain_intrinsic_ctors(obj)
14572        .into_iter()
14573        .find(|c| on_proto_or_symbol(c))
14574    {
14575        return Some(c);
14576    }
14577    // Everything else inherits `Object.prototype`'s.
14578    if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14579        return Some("Object");
14580    }
14581    None
14582}
14583
14584/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14585/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14586/// that preserves the *reference graph*. Two properties pointing at the same
14587/// object clone to two properties pointing at the same clone, and a cycle clones
14588/// to a cycle instead of recursing forever. `seen` maps each source heap index
14589/// to its clone, which is what buys both.
14590/// The rendering node puts in a `DataCloneError` for a value the structured
14591/// clone algorithm refuses, or `None` when the value IS cloneable.
14592///
14593/// Refusing at all is the point: these used to be copied through by reference,
14594/// so `structuredClone({f: () => 1})` handed back an object sharing the
14595/// original's function and `structuredClone(new WeakMap())` returned the very
14596/// same WeakMap. Node throws on every one of them.
14597fn clone_refusal(v: &Value) -> Option<String> {
14598    let kind = with_host(|h| h.kind_of(v))?;
14599    let render = |ctor: &str| Some(format!("#<{ctor}>"));
14600    match kind {
14601        // A function renders as its SOURCE TEXT here, which each FuncDef
14602        // keeps as a span into its script (`JsHost::func_source`).
14603        ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14604            Some(with_host(|h| h.str_of(v)))
14605        }
14606        ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14607            Some(with_host(|h| h.str_of(v)))
14608        }
14609        ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14610        ObjKind::Promise => render("Promise"),
14611        ObjKind::Generator => Some("[object Generator]".to_string()),
14612        // A proxy is refused by its TARGET's shape: a callable one renders like
14613        // the function it wraps, everything else as a plain object.
14614        ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14615            with_host(|h| h.str_of(v))
14616        } else {
14617            "#<Object>".to_string()
14618        }),
14619        ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14620            render("WeakMap")
14621        }
14622        ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14623            render("WeakSet")
14624        }
14625        _ => match crate::stdlib::native_tag(v).as_deref() {
14626            Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14627            _ => None,
14628        },
14629    }
14630}
14631
14632/// `structuredClone(value[, { transfer }])`.
14633///
14634/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14635/// after the clone — its bytes belong to the copy. The option used to be
14636/// ignored entirely, so the source buffer stayed usable where node leaves it
14637/// with zero length.
14638fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14639    let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14640        Some(opts) => {
14641            let t = get_property(opts, "transfer")?;
14642            if matches!(t, Value::Undef) {
14643                Vec::new()
14644            } else {
14645                host::iter_all(&t)?
14646            }
14647        }
14648        None => Vec::new(),
14649    };
14650    for item in &list {
14651        if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14652            return Err(host::dom_error(
14653                "DataCloneError",
14654                "Found invalid value in transferList.",
14655            ));
14656        }
14657    }
14658    let out = deep_clone(&arg0(&args))?;
14659    for item in &list {
14660        crate::stdlib::typedarray::detach_buffer(item);
14661    }
14662    Ok(out)
14663}
14664
14665pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14666    deep_clone_seen(v, &mut std::collections::HashMap::new())
14667}
14668
14669fn deep_clone_seen(
14670    v: &Value,
14671    seen: &mut std::collections::HashMap<u32, Value>,
14672) -> Result<Value, String> {
14673    let idx = match v {
14674        Value::Obj(i) => *i,
14675        _ => return Ok(v.clone()),
14676    };
14677    if let Some(done) = seen.get(&idx) {
14678        return Ok(done.clone());
14679    }
14680    if crate::stdlib::typedarray::is_detached(v) {
14681        return Err(host::dom_error(
14682            "DataCloneError",
14683            "An ArrayBuffer is detached and could not be cloned.",
14684        ));
14685    }
14686    if let Some(render) = clone_refusal(v) {
14687        return Err(host::dom_error(
14688            "DataCloneError",
14689            &format!("{render} could not be cloned."),
14690        ));
14691    }
14692    // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14693    // handing back the same object let a write through the clone move the
14694    // original's match cursor.
14695    if let Some((src, flags)) = with_host(|h| match h.get(v) {
14696        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14697        _ => None,
14698    }) {
14699        let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14700        let out = regexp_ctor(&args)?;
14701        seen.insert(idx, out.clone());
14702        return Ok(out);
14703    }
14704    Ok(match with_host(|h| h.get(v).cloned()) {
14705        Some(JsObj::Array(items)) => {
14706            // Register the (empty) clone BEFORE recursing so a self-reference
14707            // resolves to it.
14708            let out = with_host(|h| h.new_array(Vec::new()));
14709            seen.insert(idx, out.clone());
14710            let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14711            for x in &items {
14712                cloned.push(deep_clone_seen(x, seen)?);
14713            }
14714            with_host(|h| {
14715                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14716                    *a = cloned;
14717                }
14718                // A sparse source clones to an equally sparse array: the clone
14719                // walks own properties, so a hole is nothing to copy.
14720                h.copy_holes(v, &out, Some);
14721            });
14722            out
14723        }
14724        Some(JsObj::Object(_)) => {
14725            let out = with_host(|h| h.new_object(IndexMap::new()));
14726            seen.insert(idx, out.clone());
14727            // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14728            // walked the property map, where an accessor stores nothing, so
14729            // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14730            // key and a non-enumerable one are dropped, as node drops them.
14731            let is_error = with_host(|h| h.error_to_string(v)).is_some();
14732            let proto = clone_proto(v);
14733            let keeps_proto = !matches!(proto, CloneProto::Plain);
14734            // An ERROR clones its name, message and stack and NOTHING else —
14735            // node drops any other own property, even an enumerable one.
14736            let keys: Vec<String> = if is_error {
14737                // An ERROR clones its name, message and stack and NOTHING else —
14738                // node drops any other own property, even an enumerable one.
14739                ["name", "message", "stack"]
14740                    .iter()
14741                    .filter(|k| has_property(v, k).unwrap_or(false))
14742                    .map(|k| (*k).to_string())
14743                    .collect()
14744            } else if keeps_proto {
14745                // A preserved exotic keeps EVERY own property, including the
14746                // non-enumerable ones and the internal slots — a Date's time
14747                // value, an ArrayBuffer's `byteLength` and byte store, a typed
14748                // array's view. The enumerable-only walk dropped all of those,
14749                // so a cloned Date read `Invalid Date` and a cloned
14750                // ArrayBuffer had no `byteLength`.
14751                with_host(|h| match h.get(v) {
14752                    Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14753                    _ => Vec::new(),
14754                })
14755            } else {
14756                with_host(|h| h.own_enum_key_names(v))
14757            };
14758            let mut cloned: IndexMap<String, Value> = IndexMap::new();
14759            for k in keys {
14760                // An internal slot is read straight out of the map: it is not a
14761                // property, so a `[[Get]]` would not find it.
14762                let val = if k.starts_with("@@") {
14763                    match with_host(|h| match h.get(v) {
14764                        Some(JsObj::Object(p)) => p.get(&k).cloned(),
14765                        _ => None,
14766                    }) {
14767                        Some(val) => val,
14768                        None => continue,
14769                    }
14770                } else {
14771                    get_property(v, &k)?
14772                };
14773                cloned.insert(k, deep_clone_seen(&val, seen)?);
14774            }
14775            // The prototype survives only for the exotics the algorithm knows —
14776            // a Date, an Error, a typed array, a boxed primitive. A USER class
14777            // instance becomes a plain object, which is what node produces;
14778            // keeping every prototype made `structuredClone(new K())
14779            // instanceof K` true.
14780            with_host(|h| {
14781                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14782                    *p = cloned;
14783                }
14784                match &proto {
14785                    CloneProto::Same => {
14786                        if let Some(p) = h.proto_of(v) {
14787                            h.set_proto(&out, p);
14788                        }
14789                    }
14790                    CloneProto::Ctor(c) => {
14791                        h.ensure_error_protos();
14792                        let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14793                        if let Some(p) = p {
14794                            h.set_proto(&out, p);
14795                        }
14796                        // A Buffer clones to a plain `Uint8Array`, so the native
14797                        // tag has to change with the prototype — left alone,
14798                        // `Buffer.isBuffer` still answered true for the clone.
14799                        // A Buffer clones to a plain `Uint8Array`, so the native
14800                        // tag has to change with the prototype — left alone,
14801                        // `Buffer.isBuffer` answered true for the clone and the
14802                        // brand stayed `[object Object]`. A typed array is
14803                        // tagged `TypedArray` and names its element type in
14804                        // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14805                        if c == "Uint8Array" {
14806                            let tag = h.new_str("TypedArray");
14807                            let kind = h.new_str("Uint8Array");
14808                            if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14809                                p.insert("@@native".into(), tag);
14810                                p.insert("@@kind".into(), kind);
14811                            }
14812                        }
14813                    }
14814                    CloneProto::Plain => {}
14815                }
14816                h.copy_prop_attrs(v, &out);
14817            });
14818            out
14819        }
14820        // Map/Set are structured types: clone the entries, keep the kind.
14821        Some(JsObj::Map { entries, weak }) => {
14822            let out = with_host(|h| {
14823                h.alloc(JsObj::Map {
14824                    entries: IndexMap::new(),
14825                    weak,
14826                })
14827            });
14828            seen.insert(idx, out.clone());
14829            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14830            for (k, val) in pairs {
14831                let ck = deep_clone_seen(&k, seen)?;
14832                let cv = deep_clone_seen(&val, seen)?;
14833                let _ = map_method(&out, "set", vec![ck, cv]);
14834            }
14835            out
14836        }
14837        Some(JsObj::Set { entries, weak }) => {
14838            let out = with_host(|h| {
14839                h.alloc(JsObj::Set {
14840                    entries: IndexMap::new(),
14841                    weak,
14842                })
14843            });
14844            seen.insert(idx, out.clone());
14845            let vals: Vec<Value> = entries.values().cloned().collect();
14846            for x in vals {
14847                let cx = deep_clone_seen(&x, seen)?;
14848                let _ = set_method(&out, "add", vec![cx]);
14849            }
14850            out
14851        }
14852        // A string, a BigInt and a boxed primitive are immutable enough to
14853        // share; anything left is a value type.
14854        _ => v.clone(),
14855    })
14856}
14857
14858/// Whether a cloned object keeps the source's prototype.
14859///
14860/// The structured clone algorithm reproduces the exotics it knows and turns
14861/// everything else into a plain object — so a `Date` clones to a `Date` and a
14862/// user class instance clones to an `Object`.
14863fn clone_proto(v: &Value) -> CloneProto {
14864    // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14865    // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14866    // `Error`, not `E`.
14867    if with_host(|h| h.error_to_string(v)).is_some() {
14868        let name = get_property(v, "name")
14869            .map(|n| with_host(|h| h.str_of(&n)))
14870            .unwrap_or_else(|_| "Error".into());
14871        let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14872            name
14873        } else {
14874            "Error".to_string()
14875        };
14876        return CloneProto::Ctor(class);
14877    }
14878    match crate::stdlib::native_tag(v).as_deref() {
14879        // A Buffer is not reproduced as a Buffer: node hands back a plain
14880        // `Uint8Array` over the same bytes.
14881        Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14882        Some(_) => CloneProto::Same,
14883        // A boxed primitive keeps its wrapper; anything else — a user class
14884        // instance included — becomes a plain object.
14885        None if wrapped_primitive(v).is_some() => CloneProto::Same,
14886        None => CloneProto::Plain,
14887    }
14888}
14889
14890/// Which prototype a clone gets: the source's, a named builtin's, or none.
14891enum CloneProto {
14892    Same,
14893    Ctor(String),
14894    Plain,
14895}
14896
14897// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14898
14899/// A short `Name: message` string for an error value (used when an await
14900/// rejection unwinds as a thrown error).
14901pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14902    if let Some(JsObj::Object(props)) = h.get(v) {
14903        let name = props
14904            .get("name")
14905            .map(|x| h.str_of(x))
14906            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14907            .unwrap_or_else(|| "Error".into());
14908        if let Some(m) = props.get("message") {
14909            return format!("{name}: {}", h.str_of(m));
14910        }
14911        return name;
14912    }
14913    h.str_of(v)
14914}
14915
14916/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14917/// value)`, or `() => { throw reason }` on the reject path.
14918///
14919/// Returning the carried value directly — what this used to do — skipped both
14920/// halves. A promise returned by the callback was never awaited, so the
14921/// ordinary async-cleanup shape
14922///
14923/// ```text
14924/// work().finally(() => closeConnection()).then(next)
14925/// ```
14926///
14927/// ran `next` before the connection had closed. And the chain settled three
14928/// microtask ticks early, which is observable in ordering against any other
14929/// chain, not just against a timer.
14930///
14931/// A rejection from the callback's own promise wins over the carried value, so
14932/// no reject handler is attached here: it propagates on its own.
14933fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
14934    // PromiseResolve (27.2.4.7) returns an argument that is already a promise
14935    // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
14936    // thenable takes to adopt it, which showed up as a callback returning a
14937    // rejected promise settling one tick late against every other chain.
14938    let p = match with_host(|h| h.promise_id(&result)) {
14939        Some(_) => result,
14940        None => {
14941            let fresh = with_host(|h| h.new_promise());
14942            if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
14943                host::resolve_promise_val(pid, result);
14944            }
14945            fresh
14946        }
14947    };
14948    let cell = with_host(|h| h.new_array(vec![carried]));
14949    let idx = match cell {
14950        Value::Obj(i) => i,
14951        _ => 0,
14952    };
14953    let tag = if rethrow { "finrethrow" } else { "finret" };
14954    let thunk = make_builtin(format!("@@{tag}:{idx}"));
14955    host::promise_then(&p, thunk, Value::Undef)
14956}
14957
14958fn make_builtin(name: String) -> Value {
14959    with_host(|h| h.alloc(JsObj::Builtin(name)))
14960}
14961
14962/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
14963/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
14964///
14965/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
14966/// an EXPLICIT link, so an object on the default prototype reported `null`:
14967/// `({}).__proto__ === Object.prototype` was false while
14968/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
14969/// the three cannot drift apart again.
14970pub fn prototype_of(v: &Value) -> Value {
14971    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
14972    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
14973    // not `Function.prototype`. This is the class-side half of the subclass
14974    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
14975    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
14976        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
14977    }
14978    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
14979    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
14980    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
14981    // `ClassVal.parent`, but the link itself was invisible, so
14982    // `Object.getPrototypeOf(B) === A` read false and any library walking the
14983    // constructor chain — rather than calling a static — saw a base class.
14984    // A base class keeps the default answer below (`Function.prototype`).
14985    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
14986        if let Some(parent) = c.parent {
14987            return parent;
14988        }
14989    }
14990    // `Object.create(null)` and friends really do have a null prototype.
14991    if with_host(|h| h.has_null_proto(v)) {
14992        return with_host(|h| h.null());
14993    }
14994    // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
14995    // reported itself, because the fallback below answers by constructor name
14996    // and a plain object's is `Object` — an infinite chain to anything walking
14997    // it.
14998    if with_host(|h| h.strict_eq(v, &h.object_proto())) {
14999        return with_host(|h| h.null());
15000    }
15001    // Every OTHER builtin prototype namespace (`Array.prototype`,
15002    // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15003    // would send it back to a namespace handle for its own constructor.
15004    if matches!(
15005        with_host(|h| h.get(v).cloned()),
15006        Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15007    ) {
15008        return with_host(|h| h.object_proto());
15009    }
15010    if let Some(p) = with_host(|h| h.proto_of(v)) {
15011        return p;
15012    }
15013    // A builtin exotic with no explicit `[[Prototype]]` link reports its
15014    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15015    // Array.prototype`), which `strict_eq` compares by name. A plain object
15016    // reports the one real `Object.prototype` object.
15017    with_host(|h| {
15018        h.ensure_native_protos();
15019        match default_ctor_name(h, v) {
15020            Some("Object") => h.object_proto(),
15021            // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15022            // primitive must report that object and not a fresh namespace
15023            // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15024            // compares a thunk against the real object and reads false.
15025            Some(c) => h
15026                .native_proto(c)
15027                .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15028            None => h.null(),
15029        }
15030    })
15031}
15032
15033/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15034/// internal resolve/reject functions.
15035/// A fresh promise built through the SPECIES constructor, when a `Promise`
15036/// static was reached through a subclass.
15037///
15038/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15039/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15040/// plain promise, so nothing a subclass produced was an instance of it. The
15041/// executor is a no-op: the result is settled through its promise id, which is
15042/// what the ordinary path does too.
15043fn promise_species_create() -> Result<Option<Value>, String> {
15044    let Some(ctor) = host::current_static_this() else {
15045        return Ok(None);
15046    };
15047    if !matches!(
15048        with_host(|h| h.kind_of(&ctor)),
15049        Some(ObjKind::Class) | Some(ObjKind::Func)
15050    ) {
15051        return Ok(None);
15052    }
15053    let species = match get_property(&ctor, "@@species") {
15054        Ok(Value::Undef) => ctor,
15055        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15056        Ok(s) => s,
15057        Err(_) => ctor,
15058    };
15059    if !matches!(
15060        with_host(|h| h.kind_of(&species)),
15061        Some(ObjKind::Class) | Some(ObjKind::Func)
15062    ) {
15063        return Ok(None);
15064    }
15065    let noop = make_builtin("@@pnoop".to_string());
15066    let p = host::construct(&species, vec![noop])?;
15067    // Only usable if the subclass really produced a promise; a constructor that
15068    // returned something else has no id to settle.
15069    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15070}
15071
15072/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15073/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15074///
15075/// Distinct from `promise_species_create`, which answers for a STATIC reached
15076/// through a subclass. Here the subclass comes from the receiver itself, so
15077/// `P.resolve(1).then(f)` is also a `P`.
15078pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15079    // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15080    // which has no `constructor` entry of its own.
15081    let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15082    if !matches!(
15083        with_host(|h| h.kind_of(&ctor)),
15084        Some(ObjKind::Class) | Some(ObjKind::Func)
15085    ) {
15086        return Ok(None);
15087    }
15088    let species = match get_property(&ctor, "@@species") {
15089        Ok(Value::Undef) => ctor,
15090        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15091        Ok(s) => s,
15092        Err(_) => ctor,
15093    };
15094    if !matches!(
15095        with_host(|h| h.kind_of(&species)),
15096        Some(ObjKind::Class) | Some(ObjKind::Func)
15097    ) {
15098        return Ok(None);
15099    }
15100    let noop = make_builtin("@@pnoop".to_string());
15101    let p = host::construct(&species, vec![noop])?;
15102    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15103}
15104
15105fn new_promise(executor: Value) -> Result<Value, String> {
15106    let p = with_host(|h| h.new_promise());
15107    let id = with_host(|h| h.promise_id(&p).unwrap());
15108    let res = make_builtin(format!("@@presolve:{id}"));
15109    let rej = make_builtin(format!("@@preject:{id}"));
15110    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15111        // A throw in the executor rejects the promise.
15112        let ev = host::take_exc_or_error(&e);
15113        host::reject_promise_val(id, ev);
15114    }
15115    Ok(p)
15116}
15117
15118/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15119/// already-settled promise.
15120pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15121    promise_resolve(v)
15122}
15123
15124fn promise_resolve(v: Value) -> Result<Value, String> {
15125    if let Some(p) = promise_species_create()? {
15126        let id = with_host(|h| h.promise_id(&p).unwrap());
15127        host::resolve_promise_val(id, v);
15128        return Ok(p);
15129    }
15130    Ok(host::promise_of(&v))
15131}
15132fn promise_reject(v: Value) -> Result<Value, String> {
15133    let p = match promise_species_create()? {
15134        Some(p) => p,
15135        None => with_host(|h| h.new_promise()),
15136    };
15137    let id = with_host(|h| h.promise_id(&p).unwrap());
15138    host::reject_promise_val(id, v);
15139    Ok(p)
15140}
15141
15142/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15143/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15144/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15145/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15146/// callers that hand the resolver to an event listener.
15147pub fn pending_promise_with_resolver() -> (Value, Value) {
15148    let p = with_host(|h| h.new_promise());
15149    let id = with_host(|h| h.promise_id(&p).unwrap());
15150    let resolve = make_builtin(format!("@@presolve:{id}"));
15151    (p, resolve)
15152}
15153
15154/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15155///
15156/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15157/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15158/// without the two running together, and it escapes the punctuation that is
15159/// meaningful inside a character class or a group name.
15160fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15161    let v = arg0(&args);
15162    if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15163        return Err(host::type_error("input argument must be a string"));
15164    }
15165    let s = with_host(|h| h.str_of(&v));
15166    // Punctuation that is escaped by CODE POINT rather than with a backslash.
15167    // Measured against node over the whole ASCII range, not taken from a list:
15168    // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15169    const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15170    const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15171    let mut out = String::with_capacity(s.len());
15172    for (i, c) in s.chars().enumerate() {
15173        // A leading ASCII alphanumeric, and only a leading one.
15174        if i == 0 && c.is_ascii_alphanumeric() {
15175            out.push_str(&format!("\\x{:02x}", c as u32));
15176            continue;
15177        }
15178        if SYNTAX.contains(c) {
15179            out.push('\\');
15180            out.push(c);
15181            continue;
15182        }
15183        match c {
15184            '\t' => out.push_str("\\t"),
15185            '\n' => out.push_str("\\n"),
15186            '\u{b}' => out.push_str("\\v"),
15187            '\u{c}' => out.push_str("\\f"),
15188            '\r' => out.push_str("\\r"),
15189            _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15190                let n = c as u32;
15191                if n <= 0xff {
15192                    out.push_str(&format!("\\x{n:02x}"));
15193                } else {
15194                    out.push_str(&format!("\\u{n:04x}"));
15195                }
15196            }
15197            _ => out.push(c),
15198        }
15199    }
15200    Ok(with_host(|h| h.new_str(out)))
15201}
15202
15203/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15204/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15205/// Unicode but not to ECMAScript, and node leaves both alone.
15206fn is_regex_escape_space(c: char) -> bool {
15207    matches!(
15208        c,
15209        '\u{a0}' | '\u{1680}' | '\u{2000}'
15210            ..='\u{200a}'
15211                | '\u{2028}'
15212                | '\u{2029}'
15213                | '\u{202f}'
15214                | '\u{205f}'
15215                | '\u{3000}'
15216                | '\u{feff}'
15217    )
15218}
15219
15220/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15221/// object that merely INHERITS from `Error.prototype` is not one.
15222fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15223    let v = arg0(&args);
15224    Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15225}
15226
15227/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15228/// and `Object.prototype.toString`'s step 9 both test.
15229///
15230/// The brand is the OWN `stack` an error is built with (a `DOMException`
15231/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15232/// neither, which is why inheriting from an error prototype does not make a
15233/// value an error. Shared so the two cannot disagree — branding by a chain
15234/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15235/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15236/// on the same value already said false.
15237pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15238    match h.get(v) {
15239        Some(JsObj::Object(p)) => {
15240            p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15241        }
15242        _ => false,
15243    }
15244}
15245
15246/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15247/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15248/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15249/// costs a tick that this does not.
15250fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15251    let f = arg0(&args);
15252    // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15253    // returns a rejected promise, so the surrounding `try` never sees it.
15254    if !with_host(|h| host::is_callable(h, &f)) {
15255        // Node names the TYPE alongside the value — `number 5 is not a
15256        // function` — which the ordinary call-site message does not. A plain
15257        // object and a symbol name only the type; `null` names both.
15258        let shown = with_host(|h| {
15259            let kind = h.type_of(&f);
15260            match kind {
15261                "undefined" => "undefined".to_string(),
15262                "symbol" | "bigint" => kind.to_string(),
15263                "object" if h.is_null(&f) => "object null".to_string(),
15264                "object" => "object".to_string(),
15265                "string" => format!("string \"{}\"", h.str_of(&f)),
15266                _ => format!("{kind} {}", h.str_of(&f)),
15267            }
15268        });
15269        let p = with_host(|h| h.new_promise());
15270        let id = with_host(|h| h.promise_id(&p).unwrap());
15271        let reject = make_builtin(format!("@@preject:{id}"));
15272        let err =
15273            with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15274        host::invoke(&reject, vec![err], None)?;
15275        return Ok(p);
15276    }
15277    let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15278    let p = with_host(|h| h.new_promise());
15279    let id = with_host(|h| h.promise_id(&p).unwrap());
15280    let resolve = make_builtin(format!("@@presolve:{id}"));
15281    let reject = make_builtin(format!("@@preject:{id}"));
15282    let promise = p;
15283    match host::invoke(&f, rest, None) {
15284        Ok(v) => {
15285            host::invoke(&resolve, vec![v], None)?;
15286        }
15287        Err(e) => {
15288            // The thrown VALUE, not a re-synthesis of its rendering: a callback
15289            // that throws a `TypeError` must reject with that object, and
15290            // rebuilding it from the message string flattened it to a plain
15291            // `Error` whose message was the rendered `Uncaught TypeError: t`.
15292            let err =
15293                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15294            with_host(|h| {
15295                h.error = None;
15296                h.exc = None;
15297            });
15298            host::invoke(&reject, vec![err], None)?;
15299        }
15300    }
15301    Ok(promise)
15302}
15303
15304fn promise_with_resolvers() -> Result<Value, String> {
15305    let p = with_host(|h| h.new_promise());
15306    let id = with_host(|h| h.promise_id(&p).unwrap());
15307    let resolve = make_builtin(format!("@@presolve:{id}"));
15308    let reject = make_builtin(format!("@@preject:{id}"));
15309    let mut props: IndexMap<String, Value> = IndexMap::new();
15310    props.insert("promise".into(), p);
15311    props.insert("resolve".into(), resolve);
15312    props.insert("reject".into(), reject);
15313    Ok(with_host(|h| h.new_object(props)))
15314}
15315
15316/// A promise already rejected with `e` — what every combinator hands back when
15317/// the ITERABLE misbehaves.
15318///
15319/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15320/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15321/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15322/// and the caller saw the error at the call site instead.
15323fn rejected_promise(e: String) -> Value {
15324    let p = with_host(|h| h.new_promise());
15325    let id = with_host(|h| h.promise_id(&p).unwrap());
15326    let reject = make_builtin(format!("@@preject:{id}"));
15327    let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15328    with_host(|h| {
15329        h.error = None;
15330        h.exc = None;
15331    });
15332    let _ = host::invoke(&reject, vec![err], None);
15333    p
15334}
15335
15336#[derive(Clone, Copy)]
15337enum AllMode {
15338    All,
15339    AllSettled,
15340}
15341
15342/// `Promise.all` / `Promise.allSettled`.
15343fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15344    let items = match host::iter_all(&arg0(&args)) {
15345        Ok(v) => v,
15346        Err(e) => return Ok(rejected_promise(e)),
15347    };
15348    // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15349    // subclass the promise it hands back is an instance of that subclass.
15350    let result = match promise_species_create()? {
15351        Some(p) => p,
15352        None => with_host(|h| h.new_promise()),
15353    };
15354    let rid = with_host(|h| h.promise_id(&result).unwrap());
15355    let n = items.len();
15356    if n == 0 {
15357        let empty = with_host(|h| h.new_array(Vec::new()));
15358        host::resolve_promise_val(rid, empty);
15359        return Ok(result);
15360    }
15361    // Shared mutable accumulator via Rc<RefCell<…>>.
15362    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15363    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15364    for (i, it) in items.into_iter().enumerate() {
15365        let ap = host::promise_of(&it);
15366        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15367        let slots = slots.clone();
15368        let remaining = remaining.clone();
15369        host::subscribe_native(
15370            aid,
15371            Box::new(move |state, val| {
15372                let settled = match mode {
15373                    AllMode::All => {
15374                        if state == host::PromiseState::Rejected {
15375                            host::reject_promise_val(rid, val);
15376                            return Ok(());
15377                        }
15378                        val
15379                    }
15380                    AllMode::AllSettled => with_host(|h| {
15381                        let mut m: IndexMap<String, Value> = IndexMap::new();
15382                        if state == host::PromiseState::Rejected {
15383                            m.insert("status".into(), h.new_str("rejected"));
15384                            m.insert("reason".into(), val);
15385                        } else {
15386                            m.insert("status".into(), h.new_str("fulfilled"));
15387                            m.insert("value".into(), val);
15388                        }
15389                        h.new_object(m)
15390                    }),
15391                };
15392                slots.borrow_mut()[i] = settled;
15393                let mut r = remaining.borrow_mut();
15394                *r -= 1;
15395                if *r == 0 {
15396                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15397                    host::resolve_promise_val(rid, arr);
15398                }
15399                Ok(())
15400            }),
15401        );
15402    }
15403    Ok(result)
15404}
15405
15406/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15407fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15408    let items = match host::iter_all(&arg0(&args)) {
15409        Ok(v) => v,
15410        Err(e) => return Ok(rejected_promise(e)),
15411    };
15412    // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15413    let result = match promise_species_create()? {
15414        Some(p) => p,
15415        None => with_host(|h| h.new_promise()),
15416    };
15417    let rid = with_host(|h| h.promise_id(&result).unwrap());
15418    let n = items.len();
15419    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15420    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15421    for (i, it) in items.into_iter().enumerate() {
15422        let ap = host::promise_of(&it);
15423        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15424        let errors = errors.clone();
15425        let remaining = remaining.clone();
15426        host::subscribe_native(
15427            aid,
15428            Box::new(move |state, val| {
15429                if any {
15430                    if state == host::PromiseState::Fulfilled {
15431                        host::resolve_promise_val(rid, val);
15432                    } else {
15433                        errors.borrow_mut()[i] = val;
15434                        let mut r = remaining.borrow_mut();
15435                        *r -= 1;
15436                        if *r == 0 {
15437                            // All rejected → AggregateError carrying every reason.
15438                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15439                            let msg = with_host(|h| h.new_str("All promises were rejected"));
15440                            let agg = make_error_inner("AggregateError", &[reasons, msg]);
15441                            host::reject_promise_val(rid, agg);
15442                        }
15443                    }
15444                } else if state == host::PromiseState::Rejected {
15445                    host::reject_promise_val(rid, val);
15446                } else {
15447                    host::resolve_promise_val(rid, val);
15448                }
15449                Ok(())
15450            }),
15451        );
15452    }
15453    Ok(result)
15454}
15455
15456/// `.then` / `.catch` / `.finally` on a promise.
15457fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15458    match name {
15459        "then" => Ok(host::promise_then(
15460            recv,
15461            args.first().cloned().unwrap_or(Value::Undef),
15462            args.get(1).cloned().unwrap_or(Value::Undef),
15463        )),
15464        "catch" => Ok(host::promise_then(
15465            recv,
15466            Value::Undef,
15467            args.first().cloned().unwrap_or(Value::Undef),
15468        )),
15469        "finally" => {
15470            let cb = arg0(&args);
15471            // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15472            // as BOTH handlers, and `then` ignores a non-callable one — so the
15473            // value or reason simply passes through. Building the thunks
15474            // regardless meant `p.finally(null)` tried to call `null`.
15475            if !with_host(|h| host::is_callable(h, &cb)) {
15476                return Ok(host::promise_then(recv, cb.clone(), cb));
15477            }
15478            let i = match cb {
15479                Value::Obj(i) => i,
15480                _ => 0,
15481            };
15482            let pass = make_builtin(format!("@@finpass:{i}"));
15483            let throw = make_builtin(format!("@@finthrow:{i}"));
15484            Ok(host::promise_then(recv, pass, throw))
15485        }
15486        _ => Err(host::type_error(&format!(
15487            "promise.{name} is not a function"
15488        ))),
15489    }
15490}
15491
15492fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15493    with_host(|h| {
15494        if next_tick {
15495            h.queue_nexttick(cb, args);
15496        } else {
15497            h.queue_micro(cb, args);
15498        }
15499    });
15500}
15501
15502/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15503/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15504/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15505///
15506/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15507/// fires, so it runs until cleared and — being referenced — holds the process
15508/// open exactly as in Node.
15509fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15510    let cb = arg0(&args);
15511    let delay = if name == "setImmediate" {
15512        -1.0 // before any 0ms timeout
15513    } else {
15514        args.get(1)
15515            .map(|d| with_host(|h| h.to_number(d)))
15516            .unwrap_or(0.0)
15517            .max(0.0)
15518    };
15519    let extra = if name == "setImmediate" {
15520        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15521    } else {
15522        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15523    };
15524    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15525    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15526    let interval = (name == "setInterval").then(|| delay.max(1.0));
15527    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15528    let tag = if name == "setImmediate" {
15529        "Immediate"
15530    } else {
15531        "Timeout"
15532    };
15533    crate::stdlib::timers::new_handle(id, tag)
15534}
15535
15536/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15537/// by the bare id it coerces to (code that stored `+timer` still works).
15538fn clear_timer(v: &Value) {
15539    let id =
15540        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15541    with_host(|h| h.cancel_timer(id));
15542}