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            // Each further hop is `[[GetPrototypeOf]]` (`prototype_of`), the same
2073            // answer `Object.getPrototypeOf` gives: the recorded link alone
2074            // misses a class's parent constructor and every default prototype,
2075            // so `A.isPrototypeOf(B)` for `class B extends A` and
2076            // `Error.isPrototypeOf(RangeError)` read false.
2077            let non_null = |p: Value| Some(p).filter(|p| !with_host(|h| h.is_null(p)));
2078            let mut cur = match crate::proxy::get_prototype_of(&target)? {
2079                Some(p) => non_null(p),
2080                None if with_host(|h| crate::host::is_primitive(h, &target)) => None,
2081                None => non_null(prototype_of(&target)),
2082            };
2083            // Bounded: a chain longer than any real one is a cycle.
2084            for _ in 0..100_000 {
2085                let Some(p) = cur else { break };
2086                if with_host(|h| h.strict_eq(&p, recv)) {
2087                    return Ok(Value::Bool(true));
2088                }
2089                cur = non_null(prototype_of(&p));
2090            }
2091            Ok(Value::Bool(false))
2092        }
2093        "propertyIsEnumerable" => {
2094            let k = with_host(|h| h.str_of(&arg0(&args)));
2095            // Own *and* enumerable — a non-enumerable own slot reads false. On a
2096            // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2097            // trap, since there is no property map to enumerate.
2098            if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2099                let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2100                return Ok(Value::Bool(has));
2101            }
2102            let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2103            Ok(Value::Bool(has))
2104        }
2105        "toString" => {
2106            // An instance with a custom `toString` up the chain is handled by
2107            // call_method before reaching here; this is the default — and the
2108            // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2109            // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2110            // tagged object branded itself `[object T]` when asked one way and
2111            // `[object Object]` when converted the other (`String(o)`, `${o}`,
2112            // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2113            if let Some(t) = to_string_tag(recv)? {
2114                return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2115            }
2116            Ok(with_host(|h| {
2117                let s = h.str_of(recv);
2118                h.new_str(s)
2119            }))
2120        }
2121        // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2122        // `Invoke(this, "toString")` — no locale behavior of its own. It was
2123        // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2124        // calling it threw `is not a function` on every plain object.
2125        "toLocaleString" => {
2126            let v = host::call_method(recv, "toString", Vec::new())?;
2127            Ok(v)
2128        }
2129        "valueOf" => Ok(recv.clone()),
2130        _ => Err(host::type_error(&format!("{name} is not a function"))),
2131    }
2132}
2133
2134/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2135/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2136/// these (so the caller can try statics).
2137pub fn function_builtin_method(
2138    recv: &Value,
2139    name: &str,
2140    args: &[Value],
2141) -> Result<Option<Value>, String> {
2142    match name {
2143        "call" => {
2144            let this = args.first().cloned();
2145            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2146            Ok(Some(host::invoke(recv, rest, this)?))
2147        }
2148        "apply" => {
2149            let this = args.first().cloned();
2150            let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2151            // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2152            // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2153            // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2154            // this is written for, and both produced an empty list. A nullish
2155            // second argument means no arguments at all.
2156            let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2157                Vec::new()
2158            } else {
2159                create_list_from_array_like(&arr)?
2160            };
2161            Ok(Some(host::invoke(recv, call_args, this)?))
2162        }
2163        "bind" => {
2164            let this = args.first().cloned().unwrap_or(Value::Undef);
2165            let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2166            Ok(Some(with_host(|h| {
2167                h.alloc(JsObj::BoundFunc {
2168                    target: recv.clone(),
2169                    this,
2170                    args: pre,
2171                })
2172            })))
2173        }
2174        "toString" => Ok(Some(with_host(|h| {
2175            let s = h.str_of(recv);
2176            h.new_str(s)
2177        }))),
2178        _ => Ok(None),
2179    }
2180}
2181
2182fn is_function_method(name: &str) -> bool {
2183    matches!(name, "call" | "apply" | "bind" | "toString")
2184}
2185fn is_map_method(name: &str) -> bool {
2186    matches!(
2187        name,
2188        "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2189    )
2190}
2191fn is_set_method(name: &str) -> bool {
2192    matches!(
2193        name,
2194        "add"
2195            | "has"
2196            | "delete"
2197            | "clear"
2198            | "forEach"
2199            | "keys"
2200            | "values"
2201            | "entries"
2202            | "union"
2203            | "intersection"
2204            | "difference"
2205            | "symmetricDifference"
2206            | "isSubsetOf"
2207            | "isSupersetOf"
2208            | "isDisjointFrom"
2209    )
2210}
2211fn is_generator_method(name: &str) -> bool {
2212    matches!(name, "next" | "return" | "throw")
2213}
2214
2215/// A property read on a function/class value: own fn-props (statics, name,
2216/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2217fn function_property(recv: &Value, name: &str) -> Value {
2218    // A class static, inherited down the constructor chain.
2219    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2220        if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2221            return v;
2222        }
2223        // A class's own `name` and `length` are its own, not the builtin
2224        // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2225        // `A.length === 0`, but both were read off `Array`. Only a class that
2226        // WOULD fall through to an ancestor takes this path; a plain class keeps
2227        // the ordinary computation below.
2228        if matches!(name, "name" | "length")
2229            && with_host(|h| h.class_static(recv, name)).is_none()
2230            && with_host(|h| h.class_builtin_ancestor(recv))
2231                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2232        {
2233            if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2234                return v;
2235            }
2236            if name == "name" {
2237                let n = with_host(|h| h.callable_name(recv));
2238                return with_host(|h| h.new_str(n));
2239            }
2240            // The class's own constructor decides its arity; with no explicit
2241            // one the implicit `constructor(...args)` has length 0.
2242            let ctor = with_host(|h| match h.get(recv) {
2243                Some(JsObj::Class(c)) => c.ctor.clone(),
2244                _ => None,
2245            });
2246            return match ctor {
2247                Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2248                None => Value::Float(0.0),
2249            };
2250        }
2251        // `Symbol.species` is an accessor returning `this`, so a subclass that
2252        // does not override it IS its own species. Reading it off the builtin
2253        // ancestor below would answer with the ancestor — `A[Symbol.species]`
2254        // came back as `Array`, which sent every derived result to a plain
2255        // array.
2256        if name == "@@species"
2257            && with_host(|h| h.class_static(recv, "@@species")).is_none()
2258            && with_host(|h| h.class_builtin_ancestor(recv))
2259                .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2260        {
2261            return recv.clone();
2262        }
2263        // The chain may bottom out in a BUILTIN constructor (`class D extends
2264        // Array {}`), whose statics `class_static` cannot see — it only walks
2265        // `ClassVal.parent` links between user classes. Finish the lookup with an
2266        // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2267        if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2268            if let Ok(v) = get_property(&anc, name) {
2269                if !matches!(v, Value::Undef) {
2270                    return v;
2271                }
2272            }
2273        }
2274    } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2275        return v;
2276    }
2277    // A method inherited via the function's [[Prototype]] chain (set with
2278    // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2279    // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2280    if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2281        return v;
2282    }
2283    match name {
2284        "name" => with_host(|h| {
2285            let n = h.callable_name(recv);
2286            h.new_str(n)
2287        }),
2288        "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2289        "prototype" => ensure_fn_prototype(recv),
2290        _ if is_function_method(name) => bound_method(recv, name),
2291        _ => Value::Undef,
2292    }
2293}
2294
2295/// The `.prototype` of a function value, auto-created on first access (as Node
2296/// does for every non-arrow function) with `.constructor` linking back. Arrow
2297/// functions have no `prototype`.
2298fn ensure_fn_prototype(recv: &Value) -> Value {
2299    if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2300        return p;
2301    }
2302    // Only a constructor gets one: an arrow, a method definition and an async
2303    // function are not constructors, and a class sets its own (10.2.5).
2304    if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2305        return Value::Undef;
2306    }
2307    if !with_host(|h| h.owns_prototype(recv)) {
2308        return Value::Undef;
2309    }
2310    with_host(|h| {
2311        let proto = h.new_object(IndexMap::new());
2312        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2313            p.insert("constructor".to_string(), recv.clone());
2314        }
2315        h.hide_prop(&proto, "constructor");
2316        h.set_fn_prop(recv, "prototype", proto.clone());
2317        proto
2318    })
2319}
2320
2321/// The numeric constants a core namespace owns, in the order node reports them
2322/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2323/// list: the enumeration and the read have to agree, and they did not — every
2324/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2325/// all eight of Math's, so a member that plainly exists was invisible to any
2326/// reflective copy of the namespace.
2327///
2328/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2329/// is what separates them from the methods alongside them.
2330pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2331    const MATH: &[(&str, f64)] = &[
2332        ("E", std::f64::consts::E),
2333        ("LN10", std::f64::consts::LN_10),
2334        ("LN2", std::f64::consts::LN_2),
2335        ("LOG10E", std::f64::consts::LOG10_E),
2336        ("LOG2E", std::f64::consts::LOG2_E),
2337        ("PI", std::f64::consts::PI),
2338        ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2339        ("SQRT2", std::f64::consts::SQRT_2),
2340    ];
2341    const NUMBER: &[(&str, f64)] = &[
2342        ("MAX_VALUE", f64::MAX),
2343        // The smallest positive value a Number can hold, which is the
2344        // smallest SUBNORMAL double (`5e-324`), not Rust's
2345        // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2346        // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2347        // large.
2348        // The literal, not `f64::from_bits(1)`: that is only const-callable from
2349        // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2350        // bit pattern — the smallest positive subnormal.
2351        ("MIN_VALUE", 5e-324),
2352        ("NaN", f64::NAN),
2353        ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2354        ("POSITIVE_INFINITY", f64::INFINITY),
2355        ("MAX_SAFE_INTEGER", 9007199254740991.0),
2356        ("MIN_SAFE_INTEGER", -9007199254740991.0),
2357        ("EPSILON", f64::EPSILON),
2358    ];
2359    match ns {
2360        "Math" => MATH,
2361        "Number" => NUMBER,
2362        _ => &[],
2363    }
2364}
2365
2366/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2367/// measured on node v26.8.1:
2368///
2369/// ```text
2370/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype   w=false e=false c=false
2371/// Math.max.name, Math.max.length                       w=false e=false c=true
2372/// Math.floor, Array.from, Array.prototype.slice        w=true  e=false c=true
2373/// require('path').join                                 w=true  e=true  c=true
2374/// ```
2375///
2376/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2377/// `name`/`length` is read-only but configurable, and everything else is an
2378/// ordinary method — enumerable exactly when the namespace enumerates it, which
2379/// is what separates a core module's exports from an ECMAScript namespace's.
2380fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2381    let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2382        || key == "prototype"
2383        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2384    let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2385    // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2386    // data property, whatever the namespace's built-in members look like — the
2387    // synthesized answer reported it non-enumerable, so a monkey-patched member
2388    // described itself as one of the intrinsics.
2389    let assigned = !intrinsic_proto_member(ns, key)
2390        && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2391        && with_host(|h| h.builtin_static(ns, key).is_some());
2392    let enumerable = assigned
2393        || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2394    with_host(|h| {
2395        let mut m: IndexMap<String, Value> = IndexMap::new();
2396        m.insert("value".into(), value);
2397        m.insert(
2398            "writable".into(),
2399            Value::Bool(assigned || (!frozen && !own_fn_meta)),
2400        );
2401        m.insert("enumerable".into(), Value::Bool(enumerable));
2402        m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2403        h.new_object(m)
2404    })
2405}
2406
2407/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2408/// [`builtin_member_descriptor`], split out so `delete` can ask without
2409/// building a descriptor object.
2410/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2411/// really defines — as opposed to a name a script added. An assignment over one
2412/// of these is a `[[Set]]` and leaves its attributes alone.
2413fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2414    intrinsic_proto_members(ns).is_some_and(|members| {
2415        members
2416            .iter()
2417            .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2418    })
2419}
2420
2421fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2422    !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2423        || key == "prototype"
2424        || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2425}
2426
2427/// The value of `<ns>.<name>` when it is one of those constants.
2428fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2429    namespace_constants(ns)
2430        .iter()
2431        .find(|(k, _)| *k == name)
2432        .map(|(_, v)| *v)
2433}
2434
2435/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2436/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2437/// ECMAScript builtin defines. The generated member table records the same
2438/// distinction with its `+` prefix.
2439fn is_webidl_proto(ctor: &str) -> bool {
2440    intrinsic_proto_members(&format!("{ctor}.prototype"))
2441        .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2442}
2443
2444/// The intrinsic constructor a value's own kind implies — the prototype it
2445/// inherits with no explicit link.
2446pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2447    default_ctor_name(h, v)
2448}
2449
2450/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2451/// an object inheriting it refuses an assignment to that name.
2452pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2453    crate::arity::PROTO_READONLY
2454        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2455        .ok()
2456        .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2457}
2458
2459/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2460/// property or a method.
2461pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2462    crate::arity::PROTO_ACCESSORS
2463        .binary_search_by(|(k, _)| (*k).cmp(ctor))
2464        .ok()
2465        .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2466}
2467
2468/// The constructor whose `.prototype` IS `recv`, whichever of the two
2469/// representations it uses — a `Builtin` namespace handle or a real object.
2470pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2471    with_host(|h| match h.get(recv) {
2472        Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2473        _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2474    })
2475}
2476
2477/// The getter function of an intrinsic prototype accessor, as a first-class
2478/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2479/// hands back, and the form a library uses to borrow one.
2480fn proto_getter(ctor: &str, key: &str) -> Value {
2481    with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2482}
2483
2484/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2485/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2486/// in node even though `Map.prototype` is right there on the chain.
2487fn brand_matches(recv: &Value, ctor: &str) -> bool {
2488    if let Some(tag) = crate::stdlib::native_tag(recv) {
2489        if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2490            return true;
2491        }
2492    }
2493    match ctor {
2494        "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2495        "ArrayBuffer" => with_host(
2496            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2497        ),
2498        _ => {
2499            let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2500                Some(c) => Some(c),
2501                None => with_host(|h| default_ctor_name(h, recv)),
2502            };
2503            own == Some(ctor)
2504        }
2505    }
2506}
2507
2508thread_local! {
2509    /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2510    ///
2511    /// A getter's last step reads the value off the receiver, and when the
2512    /// receiver does not STORE it that read walks the chain, finds the same
2513    /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2514    /// stack overflowed and aborted the process. An accessor already in flight
2515    /// answers `undefined` for its own key rather than re-entering — the value
2516    /// a missing internal slot has, and what node reports for one.
2517    static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2518        const { std::cell::RefCell::new(Vec::new()) };
2519}
2520
2521/// Whether `ctor`'s `key` getter is already running further down the stack.
2522fn getter_in_flight(ctor: &str, key: &str) -> bool {
2523    GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2524}
2525
2526/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2527/// `@protoget:` thunks.
2528///
2529/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2530/// wrong: it answered `undefined` where node runs the getter, fails the brand
2531/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2532/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2533/// that would otherwise throw for every flag reads two of them back.
2534pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2535    let is_the_prototype = with_host(
2536        |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2537    );
2538    if is_the_prototype && ctor == "RegExp" {
2539        // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2540        // the getter returns rather than throwing. `source` and `flags` have
2541        // their own values there; every flag getter answers `undefined`.
2542        return Ok(match key {
2543            "source" => with_host(|h| h.new_str("(?:)".to_string())),
2544            "flags" => with_host(|h| h.new_str(String::new())),
2545            _ => Value::Undef,
2546        });
2547    }
2548    // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2549    // the individual flag properties off whatever object it is handed and
2550    // concatenates their letters, so a plain object answers `""` rather than
2551    // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2552    if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2553        if !with_host(|h| is_object_like(h, recv)) {
2554            return Err(regexp_brand_error(key, recv));
2555        }
2556        let mut out = String::new();
2557        for (prop, letter) in REGEXP_FLAG_LETTERS {
2558            let v = get_property(recv, prop)?;
2559            if with_host(|h| h.truthy(&v)) {
2560                out.push(*letter);
2561            }
2562        }
2563        return Ok(with_host(|h| h.new_str(out)));
2564    }
2565    // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2566    // the getter and the setter throw for every receiver, which is how a strict
2567    // function keeps its caller unreachable. They are not brand checks and do
2568    // not name the receiver.
2569    if ctor == "Function" && matches!(key, "arguments" | "caller") {
2570        return poison_pill_read(recv);
2571    }
2572    if !brand_matches(recv, ctor) {
2573        return Err(match ctor {
2574            "RegExp" => regexp_brand_error(key, recv),
2575            "Symbol" => {
2576                host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2577            }
2578            _ => host::type_error(&format!(
2579                "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2580                brand_receiver_string(recv)
2581            )),
2582        });
2583    }
2584    // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2585    // so that the public name can be a getter on the prototype rather than an
2586    // own enumerable property. Read it straight: the chain walk below would
2587    // find this same accessor and run it again.
2588    if let Some(v) = with_host(|h| match h.get(recv) {
2589        Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2590        _ => None,
2591    }) {
2592        return Ok(v);
2593    }
2594    GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2595    let out = get_property(recv, key);
2596    GETTERS_IN_FLIGHT.with(|g| {
2597        g.borrow_mut().pop();
2598    });
2599    out
2600}
2601
2602/// `Function.prototype.arguments`/`caller` read against `recv`.
2603///
2604/// The pill is conditional and the condition is the RECEIVER, not the reading
2605/// code: a sloppy non-arrow function answers `null` (node stopped populating
2606/// these long ago but kept them readable), and everything else — an arrow, a
2607/// strict function, a non-function — throws. Keying it on the READER's
2608/// strictness, which is what this did, made `strictFn.arguments` answer
2609/// `undefined` from sloppy code and a sloppy function throw from strict code:
2610/// wrong in both directions.
2611pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2612    if with_host(|h| h.fn_is_sloppy(recv)) {
2613        return Ok(with_host(|h| h.null()));
2614    }
2615    Err(host::type_error(POISON_PILL))
2616}
2617
2618/// The message both halves of the `arguments`/`caller` poison pill throw.
2619pub(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";
2620
2621/// How a REJECTED receiver is rendered in a brand-check message.
2622///
2623/// `no_side_effects_string` answers for most of them, but two kinds differ:
2624/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2625/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2626/// that function therefore brands. Node draws the line at whether the value is
2627/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2628/// two cases here are the ones that fall on the other side of it.
2629fn brand_receiver_string(recv: &Value) -> String {
2630    if let Some(ctor) = intrinsic_proto_of(recv) {
2631        return format!("#<{ctor}>");
2632    }
2633    match crate::stdlib::native_tag(recv).as_deref() {
2634        Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2635        _ => no_side_effects_string(recv),
2636    }
2637}
2638
2639/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2640/// concatenates their letters.
2641const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2642    ("hasIndices", 'd'),
2643    ("global", 'g'),
2644    ("ignoreCase", 'i'),
2645    ("multiline", 'm'),
2646    ("dotAll", 's'),
2647    ("unicode", 'u'),
2648    ("unicodeSets", 'v'),
2649    ("sticky", 'y'),
2650];
2651
2652/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2653/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2654/// because 22.2.6.5 reads the individual flags off any object it is given.
2655fn regexp_brand_error(key: &str, recv: &Value) -> String {
2656    if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2657        return host::type_error(&format!(
2658            "RegExp.prototype.flags getter called on non-object {}",
2659            no_side_effects_string(recv)
2660        ));
2661    }
2662    host::type_error(&format!(
2663        "RegExp.prototype.{key} getter called on non-RegExp object"
2664    ))
2665}
2666
2667/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2668/// `console.log`).
2669pub fn namespace_property(ns: &str, name: &str) -> Value {
2670    // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2671    // read sees whatever is loaded now and `delete` (see `delete_property`)
2672    // actually invalidates.
2673    if ns == REQUIRE_CACHE {
2674        return crate::module::cache_get(name).unwrap_or(Value::Undef);
2675    }
2676    // A property a SCRIPT assigned onto this namespace wins over everything
2677    // synthesized below, including a member the namespace really has. That is
2678    // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2679    // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2680    // to read back at all. Only the two `Error` hooks consulted this table, so
2681    // every other assignment onto a builtin — the whole polyfill idiom — was
2682    // stored by `set_property` and then never read: the write appeared to
2683    // succeed, `Object.isExtensible` said true, and the value came back
2684    // `undefined`.
2685    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2686        return v;
2687    }
2688    // The ENTRY script's `require` is this builtin rather than the per-module
2689    // closure, so its `cache` has to be handed out here too.
2690    // `require.extensions` — the legacy loader map. Deprecated but still read
2691    // (and sometimes written) by tooling that hooks module loading, and it was
2692    // absent entirely. The three keys node ships are present; installing a
2693    // custom loader through them is NOT honoured by this runtime's loader, so
2694    // the map reports what it can serve rather than pretending otherwise.
2695    // `util.promisify.custom` — the registered symbol a module attaches to a
2696    // callback function to supply its own promisified form. It was `undefined`,
2697    // so the lookup that decides whether to use one always missed.
2698    if ns == "util.promisify" && name == "custom" {
2699        return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2700    }
2701    // `process.memoryUsage.rss()` — node's fast path for the one figure that
2702    // does not need the whole object built.
2703    if ns == "process.memoryUsage" && name == "rss" {
2704        return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2705    }
2706    if ns == "require" && name == "extensions" {
2707        return with_host(|h| {
2708            let mut m: IndexMap<String, Value> = IndexMap::new();
2709            for ext in [".js", ".json", ".node"] {
2710                let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2711                m.insert(ext.to_string(), f);
2712            }
2713            h.new_object(m)
2714        });
2715    }
2716    // `require.resolve.paths(spec)` — the directories a lookup would search:
2717    // `null` for a core module, the `node_modules` chain otherwise.
2718    if ns == "require.resolve" && name == "paths" {
2719        return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2720    }
2721    if ns == "require" && name == "cache" {
2722        return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2723    }
2724    // The legacy numeric codes `DOMException` carries as statics
2725    // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2726    if ns == "DOMException" {
2727        if let Some((_, code)) = DOM_EXCEPTION_CODES
2728            .iter()
2729            .find(|(n, _)| legacy_code_name(n) == name)
2730        {
2731            return Value::Float(*code);
2732        }
2733    }
2734    // Numeric constants.
2735    if let Some(k) = namespace_constant(ns, name) {
2736        return Value::Float(k);
2737    }
2738    // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2739    // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2740    // `undefined`.
2741    // `GeneratorFunction.prototype` and the two async variants are REAL objects
2742    // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2743    // sit on the prototype chain of every generator/async function, which a
2744    // handle cannot do. Without this the read fell through to `undefined`.
2745    if name == "prototype"
2746        && matches!(
2747            ns,
2748            "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2749        )
2750    {
2751        return with_host(|h| {
2752            h.ensure_native_protos();
2753            h.native_proto(ns).unwrap_or(Value::Undef)
2754        });
2755    }
2756    // `Error.prepareStackTrace` has a DEFAULT hook in node
2757    // (`ErrorPrepareStackTrace`), so a library probing `if
2758    // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2759    // down the wrong branch. The default renders the header plus the frames,
2760    // which is what the fast path in `materialize_stack` already produces — it
2761    // recognises this exact builtin and skips the round trip.
2762    if ns == "Error" && name == "prepareStackTrace" {
2763        return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2764            || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2765        );
2766    }
2767    // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2768    // lands in the builtin-static side table, which the read below consults
2769    // first. Without a default the READ was `undefined`, so a library doing
2770    // `const old = Error.stackTraceLimit` and restoring it later installed
2771    // `undefined` and disabled the limit permanently.
2772    if ns == "Error" && name == "stackTraceLimit" {
2773        return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2774            .unwrap_or(Value::Float(10.0));
2775    }
2776    // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2777    // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2778    // protocol had nothing to read and every derived result came back a plain
2779    // builtin.
2780    if name == "@@species" && has_species(ns) {
2781        return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2782    }
2783    if name == "name" && is_builtin_ctor(ns) {
2784        return with_host(|h| h.new_str(ns.to_string()));
2785    }
2786    // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2787    // computed property/method key.
2788    if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2789        return with_host(|h| h.well_known_symbol(name));
2790    }
2791    // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2792    // `buffer.Buffer`, `url.URL`).
2793    if let Some(v) = crate::stdlib::constant(ns, name) {
2794        return v;
2795    }
2796    // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2797    // `Array.prototype`, …): a prototype namespace whose methods are callable
2798    // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2799    // `get-intrinsic`/`function-bind` family).
2800    if name == "prototype" && is_builtin_ctor(ns) {
2801        // Same reasoning as the native prototypes below, for the error
2802        // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2803        // `error_protos` object, so `Error.prototype` has to read back that same
2804        // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2805        // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2806        // Error("x")) === Error.prototype` was false, and `typeof
2807        // Error.prototype` was `"function"` where node says `"object"`.
2808        if host::ERROR_NAMES.contains(&ns) {
2809            if let Some(p) = with_host(|h| {
2810                h.ensure_error_protos();
2811                host::error_proto_of(h, ns)
2812            }) {
2813                return p;
2814            }
2815        }
2816        // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2817        // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2818        // Buffer.prototype` must compare equal, which a freshly-allocated
2819        // `Builtin` handle never can.
2820        if let Some(p) = with_host(|h| {
2821            h.ensure_native_protos();
2822            h.native_proto(ns)
2823        }) {
2824            return p;
2825        }
2826        let _ = ns;
2827        return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2828    }
2829    // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2830    // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2831    // above never fired and the read produced `undefined` — which broke the ES5
2832    // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2833    // reads `StringDecoder.prototype.end` at load, and threw
2834    // `Cannot read properties of undefined (reading 'end')`. Built from the same
2835    // instance-method table a method read consults, so the two cannot disagree.
2836    if name == "prototype" {
2837        if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2838            return p;
2839        }
2840    }
2841    // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2842    // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2843    // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2844    //
2845    // The thunk is minted only for a name the prototype REALLY carries. Minting
2846    // one unconditionally made every absent name answer with a function:
2847    // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2848    // says `undefined`, and so was every well-known symbol a prototype does not
2849    // define — `Array.prototype[Symbol.toStringTag]` came back a function
2850    // instead of `undefined`, which is a value `Object.prototype.toString` and
2851    // every `typeof`/truthiness test downstream then read wrong.
2852    //
2853    // Existence is decided by the generated intrinsic table, which is read out
2854    // of the reference engine, so this cannot drift from what node defines.
2855    // A name the prototype does not define but `Object.prototype` does is
2856    // INHERITED, and node hands back Object.prototype's own function object
2857    // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2858    // resolves to the `Object` thunk rather than a per-ctor one. That is also
2859    // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2860    // has no own `toString`, and the inherited one is the generic tag reader,
2861    // not a Map method that rejects a non-Map `this`.
2862    if let Some(ctor) = ns.strip_suffix(".prototype") {
2863        // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2864        // not an intrinsic function, so it is not in the arity table the lookup
2865        // above consults. It lists the methods a `with` block must NOT bring
2866        // into scope — the ones added after `with` existed, so old code using a
2867        // variable of the same name keeps working.
2868        if name == "@@unscopables" && ctor == "Array" {
2869            return with_host(|h| {
2870                let mut m: IndexMap<String, Value> = IndexMap::new();
2871                for k in [
2872                    "at",
2873                    "copyWithin",
2874                    "entries",
2875                    "fill",
2876                    "find",
2877                    "findIndex",
2878                    "findLast",
2879                    "findLastIndex",
2880                    "flat",
2881                    "flatMap",
2882                    "includes",
2883                    "keys",
2884                    "toReversed",
2885                    "toSorted",
2886                    "toSpliced",
2887                    "values",
2888                ] {
2889                    m.insert(k.to_string(), Value::Bool(true));
2890                }
2891                let o = h.new_object(m);
2892                let null = h.null();
2893                h.set_proto(&o, null);
2894                o
2895            });
2896        }
2897        if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2898            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2899        }
2900        if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2901            return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2902        }
2903        // `constructor` is excluded from the table because it is not a method:
2904        // it is the constructor function itself, and node compares equal
2905        // (`Array.prototype.constructor === Array`). It used to resolve to a
2906        // `@proto:Array:constructor` thunk, which is a different object every
2907        // read and so never compared equal to anything.
2908        if name == "constructor" && is_builtin_ctor(ctor) {
2909            return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2910        }
2911        return Value::Undef;
2912    }
2913    let qualified = format!("{ns}.{name}");
2914    if is_known_builtin(&qualified) {
2915        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2916    }
2917    // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2918    if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2919        return v;
2920    }
2921    // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2922    // both). `Math.max.name` was `undefined` — as was every `.name` a library
2923    // reads to identify a callback it was handed. The non-callable namespaces
2924    // fall through: `Math.name` and `require('fs').length` really are undefined.
2925    if host::builtin_is_callable(ns) {
2926        match name {
2927            "name" => {
2928                if let Some(n) = proto_getter_name(ns) {
2929                    return with_host(|h| h.new_str(n));
2930                }
2931                return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2932            }
2933            // Only the intrinsics have a specified arity; a core-module
2934            // function's is a property of node's own JS source, so it stays
2935            // `undefined` rather than being invented here.
2936            "length" => {
2937                // A getter takes no argument (10.2.9 / the accessor grammar),
2938                // so its `length` is 0 — it is not in the intrinsic table,
2939                // which holds only named functions.
2940                if proto_getter_name(ns).is_some() {
2941                    return Value::Float(0.0);
2942                }
2943                if let Some((_, len)) = builtin_meta(ns) {
2944                    return Value::Float(len as f64);
2945                }
2946            }
2947            _ => {}
2948        }
2949    }
2950    Value::Undef
2951}
2952
2953/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2954/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2955/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2956/// libraries type-check on; every other method routes through normal method
2957/// dispatch on `recv`.
2958/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2959/// with NO receiver — `const f = [].push; f(1)`.
2960///
2961/// Reading a method off an instance used to mint a thunk bound to that
2962/// instance, so a detached method silently kept working on the object it came
2963/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2964/// to say so. Node words it four ways, and which one a method gets is not
2965/// something that can be derived — the split was measured across every method
2966/// of each prototype:
2967///
2968/// ```text
2969/// ToObject(this)         "Cannot convert undefined or null to object"
2970/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2971/// brand check            "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2972/// everything else        the generic incompatible-receiver message
2973/// ```
2974fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2975    // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2976    // the two `indexOf` family members name themselves, the rest go through
2977    // `ToObject` and report its message.
2978    const ARRAY_NAMED: &[&str] = &[
2979        "concat",
2980        "every",
2981        "filter",
2982        "find",
2983        "findIndex",
2984        "findLast",
2985        "findLastIndex",
2986        "forEach",
2987        "indexOf",
2988        "map",
2989        "reduce",
2990        "reduceRight",
2991        "some",
2992    ];
2993    const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2994    let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2995    let branded =
2996        |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2997    // The generic form names the receiver, so a `null` one must not be reported
2998    // as `undefined`.
2999    let generic = |c: &str, m: &str| {
3000        format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
3001    };
3002    Some(match ctor {
3003        "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
3004        "Array" => TO_OBJECT.to_string(),
3005        // `Object.prototype.toString` is the one method that ACCEPTS a nullish
3006        // receiver — it answers `[object Undefined]`.
3007        "Object" if method == "toString" => return None,
3008        "Object" if method == "toLocaleString" => named("Object"),
3009        "Object" => TO_OBJECT.to_string(),
3010        // Both aliases report the LEGACY name in the message, which is the one
3011        // place `name` and the message disagree.
3012        "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
3013        "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
3014        "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
3015        "String" => named("String"),
3016        "Number" => branded("Number", "Number"),
3017        "Boolean" => branded("Boolean", "Boolean"),
3018        "Symbol" => branded("Symbol", "Symbol"),
3019        "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3020        "Function" if matches!(method, "call" | "apply") => format!(
3021            "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3022        ),
3023        "Function" => branded("Function", "Function"),
3024        // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3025        // a nullish receiver fails inside them and reports that instead.
3026        "Promise" if method == "catch" => {
3027            "Cannot read properties of undefined (reading 'then')".to_string()
3028        }
3029        "Promise" if method == "finally" => {
3030            "Promise.prototype.finally called on non-object".to_string()
3031        }
3032        "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3033        // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3034        // report that slot check; every setter, every `to*String` and the two
3035        // legacy year methods go through the generic receiver check first.
3036        "Date"
3037            if method == "valueOf"
3038                || (method.starts_with("get") && method != "getYear") =>
3039        {
3040            "this is not a Date object.".to_string()
3041        }
3042        // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3043        // and `Set.prototype.keys` IS `values`, one function object each.
3044        "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3045        "Set" if method == "keys" => generic("Set", "values"),
3046        // Everything else that is brand-checked names itself. Node reaches this
3047        // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3048        // is the receiver's kind, and only the message has to agree.
3049        "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3050        | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3051        "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3052        // Node's `URL` methods fail while reaching for their internal state, and
3053        // report the read that failed rather than the method.
3054        "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3055        _ => return None,
3056    })
3057}
3058
3059/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3060/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3061/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3062/// are generic and coerce their receiver with `ToString`.
3063fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3064    match ctor {
3065        "Number" => matches!(
3066            method,
3067            "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3068        ),
3069        "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3070        "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3071        _ => false,
3072    }
3073}
3074
3075/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3076/// primitive itself, the primitive a wrapper boxes, or — for the three
3077/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3078/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3079fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3080    let expected = match ctor {
3081        "Number" => "number",
3082        "String" => "string",
3083        "Boolean" => "boolean",
3084        "BigInt" => "bigint",
3085        _ => return None,
3086    };
3087    let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3088    if is_expected(recv) {
3089        return Some(recv.clone());
3090    }
3091    if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3092        return Some(prim);
3093    }
3094    if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3095        return match ctor {
3096            "Number" => Some(Value::Float(0.0)),
3097            "String" => Some(with_host(|h| h.new_str(""))),
3098            "Boolean" => Some(Value::Bool(false)),
3099            _ => None,
3100        };
3101    }
3102    None
3103}
3104
3105pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3106    let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3107    // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3108    // the instance's hidden `@@<name>` slot, which is where the value lives now
3109    // that the public name is a getter rather than an own property.
3110    if let Some(key) = method.strip_prefix("@get@") {
3111        if let Some(v) = with_host(|h| match h.get(recv) {
3112            Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3113            _ => None,
3114        }) {
3115            return Ok(v);
3116        }
3117        // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3118        // `symmetricKeySize` is the secret's byte length, which nothing stores.
3119        let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3120        return crate::stdlib::instance_call(&tag, recv, method, args);
3121    }
3122    if let Some(key) = method.strip_prefix("@set@") {
3123        let v = args.first().cloned().unwrap_or(Value::Undef);
3124        with_host(|h| {
3125            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3126                p.insert(format!("@@{key}"), v);
3127            }
3128        });
3129        crate::stdlib::instance_accessor_written(ctor, key, recv);
3130        return Ok(Value::Undef);
3131    }
3132    if with_host(|h| h.is_nullish(recv)) {
3133        let shown = if with_host(|h| h.is_null(recv)) {
3134            "null"
3135        } else {
3136            "undefined"
3137        };
3138        if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3139            return Err(format!("TypeError: {msg}"));
3140        }
3141    }
3142    // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3143    // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3144    if ctor == "Error" && method == "toString" {
3145        // A `DOMException` keeps its `name`/`message` in internal slots, so the
3146        // chain read below would find the class name on the prototype instead.
3147        if let Some(n) = dom_exception_slot(recv, "name") {
3148            let name = with_host(|h| h.str_of(&n));
3149            let msg = dom_exception_slot(recv, "message")
3150                .map(|m| with_host(|h| h.str_of(&m)))
3151                .unwrap_or_default();
3152            let s = if msg.is_empty() {
3153                name
3154            } else {
3155                format!("{name}: {msg}")
3156            };
3157            return Ok(with_host(|h| h.new_str(s)));
3158        }
3159        // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3160        // so a PROXY supplies them through its `get` trap. Reading the stored
3161        // ones first made `String(new Proxy(err, handler))` ignore the handler.
3162        let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3163        let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3164        let s = match stored.flatten() {
3165            Some(s) => s,
3166            None => {
3167                let read = |k: &str| -> Result<Option<String>, String> {
3168                    Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3169                };
3170                let name = read("name")?.unwrap_or_else(|| "Error".into());
3171                let msg = read("message")?.unwrap_or_default();
3172                if msg.is_empty() {
3173                    name
3174                } else {
3175                    format!("{name}: {msg}")
3176                }
3177            }
3178        };
3179        return Ok(with_host(|h| h.new_str(s)));
3180    }
3181    // The methods that read their receiver through `thisNumberValue` /
3182    // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3183    // primitive, its wrapper, or the prototype object (which carries the zero
3184    // value) — anything else is a TypeError naming the method. Unchecked,
3185    // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3186    // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3187    // recursed through the generic conversion until the stack overflowed.
3188    if is_brand_checked_primitive_method(ctor, method) {
3189        let Some(prim) = this_primitive_value(ctor, recv) else {
3190            return Err(format!(
3191                "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3192            ));
3193        };
3194        return host::call_method(&prim, method, args);
3195    }
3196    // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3197    // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3198    // arrives with an already-primitive receiver and needs no unwrapping.
3199    if matches!(ctor, "String" | "Number" | "Boolean") {
3200        let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3201        return host::call_method(&prim, method, args);
3202    }
3203    // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3204    // readily as the primitive, and neither was unwrapped here. A BigInt
3205    // wrapper's `valueOf` therefore re-entered the generic conversion, which
3206    // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3207    // until the stack overflowed and ABORTED the process, which no try/catch can
3208    // see. A Symbol wrapper failed the brand check below instead and reported
3209    // that `this` was not a Symbol, when it is one. Only a real wrapper is
3210    // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3211    // the brand check.
3212    if matches!(ctor, "Symbol" | "BigInt") {
3213        if let Some(prim) = wrapped_primitive(recv) {
3214            return host::call_method(&prim, method, args);
3215        }
3216    }
3217    if ctor == "Object" && method == "toString" {
3218        // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3219        // (own or inherited, data property or getter) replaces the builtin brand,
3220        // which is how a class advertises its own (`class C { get
3221        // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3222        // runs outside the host borrow so an accessor can be invoked.
3223        // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3224        // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3225        // the ordinary receiver does, to keep the read off objects that have no
3226        // tag) would always miss and brand every tagged proxy `[object Object]`.
3227        if let Some(s) = to_string_tag(recv)? {
3228            return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3229        }
3230        return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3231    }
3232    // These thunks now live on the real `Object.prototype` object, i.e. on the
3233    // receiver's own chain — routing back through `call_method` would re-resolve
3234    // this very thunk and recurse.
3235    if ctor == "Object" && is_object_builtin_method(method) {
3236        return object_builtin_method(recv, method, args);
3237    }
3238    // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3239    // emitter method directly against `recv` (routing back through `call_method`
3240    // would re-resolve the mixed-in thunk and recurse).
3241    if ctor == "EventEmitter" {
3242        return crate::stdlib::events::instance_call(recv, method, args);
3243    }
3244    // Same recursion hazard for the exotics with a real prototype object: the
3245    // thunk now lives ON the receiver's prototype chain, so `call_method` would
3246    // re-resolve this very thunk. Dispatch straight to the native instance
3247    // implementation when the receiver is in fact an instance of `ctor`.
3248    if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3249        return crate::stdlib::buffer::instance_call(recv, method, &args);
3250    }
3251    // The shared typed-array methods now live on the `%TypedArray%.prototype`
3252    // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3253    // appears for anything read directly off `Uint8Array.prototype`. Both
3254    // dispatch the same way, and both must bypass `call_method` or the thunk
3255    // would re-resolve itself off the receiver's chain and recurse.
3256    if ctor == "Uint8Array" || ctor == "TypedArray" {
3257        match crate::stdlib::native_tag(recv).as_deref() {
3258            Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3259            Some("TypedArray") => {
3260                return crate::stdlib::typedarray::instance_call(recv, method, &args)
3261            }
3262            _ => {}
3263        }
3264    }
3265    // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3266    // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3267    // `LengthOfArrayLike`), which is what makes
3268    // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3269    // is not an Array, so `call_method` would report the method missing.
3270    if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3271        return array_generic(recv, method, args);
3272    }
3273    // The general form of the two special cases above: a thunk taken off a native
3274    // constructor's real prototype, invoked with a receiver that IS an instance of
3275    // that constructor. Routing back through `call_method` would re-resolve this
3276    // very thunk off the receiver's own chain and recurse forever, which is why
3277    // each such prototype needed a hand-written bypass; now they all have one.
3278    // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3279    // through its chain, and requiring an exact tag match sent that call back
3280    // into `call_method`, which re-resolved this same thunk and recursed until
3281    // the stack overflowed.
3282    if let Some(tag) = crate::stdlib::native_tag(recv) {
3283        let mut c = Some(tag.as_str());
3284        while let Some(t) = c {
3285            if t == ctor {
3286                return crate::stdlib::instance_call(&tag, recv, method, args);
3287            }
3288            c = crate::stdlib::native_parent(t);
3289        }
3290    }
3291    // A BRANDED method reached with a receiver that has no such internal slot.
3292    // Every arm above dispatches a receiver that IS an instance, so arriving
3293    // here with one of these constructors means the brand check failed — the
3294    // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3295    // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3296    // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3297    // `union is not a function`, which says the method does not exist rather
3298    // than that the receiver is the wrong kind of object.
3299    // `Date.prototype`'s methods split in two: the ones that read the time value
3300    // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3301    // rest take the ordinary branded form. Measured on node v26.8.1:
3302    // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3303    // and `.setHours.call({})` the second.
3304    if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3305        const THIS_TIME_VALUE: &[&str] = &[
3306            "getTime",
3307            "valueOf",
3308            "getYear",
3309            "getFullYear",
3310            "getMonth",
3311            "getDate",
3312            "getDay",
3313            "getHours",
3314            "getMinutes",
3315            "getSeconds",
3316            "getMilliseconds",
3317            "getUTCFullYear",
3318            "getUTCMonth",
3319            "getUTCDate",
3320            "getUTCDay",
3321            "getUTCHours",
3322            "getUTCMinutes",
3323            "getUTCSeconds",
3324            "getUTCMilliseconds",
3325            "getTimezoneOffset",
3326        ];
3327        if THIS_TIME_VALUE.contains(&method) {
3328            return Err(host::type_error("this is not a Date object."));
3329        }
3330        // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3331        // receiver and INVOKES `toISOString` on it, so it fails on the missing
3332        // method rather than on a brand.
3333        if method != "toJSON" {
3334            return Err(host::type_error(&format!(
3335                "Method Date.prototype.{method} called on incompatible receiver {}",
3336                no_side_effects_string(recv)
3337            )));
3338        }
3339    }
3340    // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3341    // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3342    // check the receiver at the call boundary take the branded form. Measured
3343    // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3344    // genuinely generic (it is `Array.prototype.toString`) and never brands.
3345    if matches!(ctor, "TypedArray" | "Uint8Array")
3346        && !matches!(
3347            crate::stdlib::native_tag(recv).as_deref(),
3348            Some("TypedArray") | Some("Buffer")
3349        )
3350    {
3351        const BRANDED: &[&str] = &[
3352            "slice",
3353            "subarray",
3354            "join",
3355            "sort",
3356            "at",
3357            "toReversed",
3358            "toSorted",
3359            "toLocaleString",
3360        ];
3361        if BRANDED.contains(&method) {
3362            return Err(host::type_error(&format!(
3363                "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3364                no_side_effects_string(recv)
3365            )));
3366        }
3367        // The four base64/hex methods brand themselves against `Uint8Array`
3368        // specifically — a WRONG view is as incompatible as a plain object, and
3369        // the generic guard here cannot tell those apart.
3370        if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3371            return Err(host::type_error(&format!(
3372                "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3373                no_side_effects_string(recv)
3374            )));
3375        }
3376        if method != "toString" {
3377            return Err(host::type_error("this is not a typed array."));
3378        }
3379    }
3380    // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3381    // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3382    // that back to `call_method` read `call` off the proxy again, which
3383    // resolved to this same thunk, and recursed until the stack overflowed and
3384    // aborted the process. The three are defined on the callee alone, so they
3385    // run here: the proxy's `apply` trap (or its target) gets the call.
3386    // `toString` recursed the same way.
3387    if ctor == "Function"
3388        && matches!(method, "call" | "apply" | "bind" | "toString")
3389        && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3390    {
3391        let mut rest = args.into_iter();
3392        let this_arg = rest.next().unwrap_or(Value::Undef);
3393        match method {
3394            "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3395            "apply" => {
3396                let list = match rest.next() {
3397                    None | Some(Value::Undef) => Vec::new(),
3398                    Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3399                    Some(v) => create_list_from_array_like(&v)?,
3400                };
3401                return host::invoke(recv, list, Some(this_arg));
3402            }
3403            "bind" => {
3404                let target = recv.clone();
3405                let pre: Vec<Value> = rest.collect();
3406                return Ok(with_host(|h| {
3407                    h.alloc(JsObj::BoundFunc {
3408                        target,
3409                        this: this_arg,
3410                        args: pre,
3411                    })
3412                }));
3413            }
3414            // A proxy has no source text; V8 prints the native form for it.
3415            _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3416        }
3417    }
3418    // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3419    // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3420    // object. Without this check `Symbol.prototype.toString()` re-entered the
3421    // generic string conversion, which looked `toString` up again and called it
3422    // again — an infinite recursion that overflowed the stack and ABORTED the
3423    // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3424    // The wording is Symbol's own, not the "incompatible receiver" form the
3425    // collections use.
3426    if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3427        // A symbol-KEYED method is named in brackets rather than after a dot:
3428        // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3429        // That is the message `String(Symbol.prototype)` produces, since the
3430        // conversion reaches `@@toPrimitive` before it would reach `toString`.
3431        let named = match method.strip_prefix("@@") {
3432            Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3433            None => format!("Symbol.prototype.{method}"),
3434        };
3435        return Err(host::type_error(&format!(
3436            "{named} requires that 'this' be a Symbol"
3437        )));
3438    }
3439    if let Some(label) = branded_method_label(ctor, recv) {
3440        return Err(host::type_error(&format!(
3441            "Method {label}.prototype.{method} called on incompatible receiver {}",
3442            no_side_effects_string(recv)
3443        )));
3444    }
3445    host::call_method(recv, method, args)
3446}
3447
3448/// The name a branded prototype method reports itself under when its receiver
3449/// fails the brand check, or `None` when `ctor`'s methods are generic over
3450/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3451/// receiver really is an instance.
3452///
3453fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3454    let kind = with_host(|h| h.kind_of(recv));
3455    // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3456    // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3457    // even though both are `JsObj::Set` here.
3458    let weak = peek(recv, |o| match o {
3459        JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3460        _ => None,
3461    })
3462    .unwrap_or(false);
3463    let ok = match ctor {
3464        "Set" => kind == Some(ObjKind::Set) && !weak,
3465        "WeakSet" => kind == Some(ObjKind::Set) && weak,
3466        "Map" => kind == Some(ObjKind::Map) && !weak,
3467        "WeakMap" => kind == Some(ObjKind::Map) && weak,
3468        "Promise" => kind == Some(ObjKind::Promise),
3469        _ => return None,
3470    };
3471    if ok {
3472        return None;
3473    }
3474    Some(match ctor {
3475        "Set" => "Set",
3476        "WeakSet" => "WeakSet",
3477        "Map" => "Map",
3478        "WeakMap" => "WeakMap",
3479        _ => "Promise",
3480    })
3481}
3482
3483/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3484/// uses for a value it must not run user code on. Measured on node v26.8.1
3485/// through `Map.prototype.get.call(x)`:
3486///
3487/// ```text
3488/// 5 / 'str' / true / null / undefined / 9n   the value's own ToString
3489/// Symbol('s')                                Symbol(s)
3490/// function f(){}                             its source text
3491/// new Error('e')                             Error: e
3492/// {} / new (class A {})                      #<Object> / #<A>
3493/// new Map() / Promise.resolve()              #<Map> / #<Promise>
3494/// [] / new Date() / /re/ / new Uint8Array()  [object Array] / [object Date] / …
3495/// { toString() {} } / Object.create(null)    [object Object]
3496/// ```
3497///
3498/// The split is one test: a receiver whose `toString` is still
3499/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3500/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3501/// which is why an object with its own `toString` prints `[object Object]` and
3502/// not what that method returns.
3503fn no_side_effects_string(recv: &Value) -> String {
3504    if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3505        return with_host(|h| h.str_of(recv));
3506    }
3507    if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3508        return s;
3509    }
3510    // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3511    // rather than inside it.
3512    let native = crate::stdlib::native_tag(recv).is_some();
3513    let brands_itself = with_host(|h| {
3514        // `Object.prototype.toString` reaches every object as a thunk on the
3515        // real prototype object, so its presence proves nothing; only a
3516        // toString the receiver's chain OVERRIDES it with counts.
3517        let overridden = host::lookup_chain(h, recv, "toString")
3518            .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3519            .unwrap_or(false);
3520        native
3521            || overridden
3522            || h.has_null_proto(recv)
3523            || !matches!(
3524                h.kind_of(recv),
3525                Some(ObjKind::Object)
3526                    | Some(ObjKind::Map)
3527                    | Some(ObjKind::Set)
3528                    | Some(ObjKind::Promise)
3529            )
3530    });
3531    if brands_itself {
3532        return with_host(|h| object_tag(h, recv));
3533    }
3534    let ctor = get_property(recv, "constructor")
3535        .ok()
3536        .map(|c| with_host(|h| h.callable_name(&c)))
3537        .filter(|n| !n.is_empty())
3538        .unwrap_or_else(|| "Object".to_string());
3539    format!("#<{ctor}>")
3540}
3541
3542/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3543/// one, or `None` when reading that symbol must yield `undefined`.
3544///
3545/// Every builtin brand is already computed in exactly one place (`object_tag`),
3546/// so this reuses it and subtracts the legacy builtins, which brand for
3547/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3548/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3549/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3550/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3551/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3552/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3553/// `TextEncoder`/`TextDecoder` all read their brand.
3554pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3555    // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3556    // of which `object_tag` already brands.
3557    let tag = object_brand(h, v);
3558    const NO_TAG: &[&str] = &[
3559        "Undefined",
3560        "Null",
3561        "Boolean",
3562        "Number",
3563        "String",
3564        "Array",
3565        "Function",
3566        "Object",
3567        "Date",
3568        "RegExp",
3569        "Error",
3570    ];
3571    if NO_TAG.contains(&tag.as_str()) {
3572        return None;
3573    }
3574    Some(tag)
3575}
3576
3577/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3578/// carries an own `Symbol.toStringTag`, if any.
3579fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3580    let mut cur = h.proto_of(v);
3581    for _ in 0..100 {
3582        let p = cur?;
3583        if h.is_null(&p) {
3584            return None;
3585        }
3586        let name = match h.get(&p) {
3587            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3588            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3589        }
3590        // A CLASS prototype is not linked to the builtin its class extends —
3591        // the relationship lives on the class value — so the walk crosses over
3592        // there, or `Object.create(D.prototype)` for `class D extends Map`
3593        // finds nothing.
3594        .or_else(|| {
3595            h.class_owning_proto(&p)
3596                .and_then(|c| h.class_builtin_ancestor(&c))
3597                .map(|b| h.callable_name(&b))
3598                .filter(|n| !n.is_empty())
3599        });
3600        if let Some(n) = name {
3601            if intrinsic_proto_members(&format!("{n}.prototype"))
3602                .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3603            {
3604                return Some(n);
3605            }
3606        }
3607        cur = h.proto_of(&p);
3608    }
3609    None
3610}
3611
3612/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3613/// Every builtin exotic object reports its own brand, which is how packages
3614/// type-test values they did not construct (`toString.call(x) ===
3615/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3616/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3617pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3618    format!("[object {}]", object_brand(h, v))
3619}
3620
3621/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3622/// …), without the `[object …]` wrapper. Split out so the brand and the
3623/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3624/// Whether `v` is a function's `arguments` object.
3625///
3626/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3627/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3628/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3629pub fn is_arguments(v: &Value) -> bool {
3630    with_host(|h| is_arguments_h(h, v))
3631}
3632
3633/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3634/// runs under one, and re-entering through `with_host` aborts the process.
3635pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3636    h.fn_prop(v, "@@arguments").is_some()
3637}
3638
3639fn object_brand(h: &host::JsHost, v: &Value) -> String {
3640    // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3641    // it holds no instance slot, so only the branded few report anything but
3642    // `[object Object]`. Checked before the match because those prototypes are
3643    // plain `JsObj::Object`s and would otherwise be branded by whatever their
3644    // own properties happen to look like — `TypeError.prototype` has `name` and
3645    // `message`, which read as an Error instance.
3646    if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3647        return if BRANDED_PROTOS.contains(&ctor) {
3648            ctor.to_string()
3649        } else {
3650            "Object".to_string()
3651        };
3652    }
3653    let tag: String = match v {
3654        Value::Undef => "Undefined".into(),
3655        Value::Bool(_) => "Boolean".into(),
3656        Value::Int(_) | Value::Float(_) => "Number".into(),
3657        Value::Str(_) => "String".into(),
3658        Value::Obj(_) => match h.get(v) {
3659            Some(JsObj::Null) => "Null".into(),
3660            Some(JsObj::Str(_)) => "String".into(),
3661            Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3662            Some(JsObj::Array(_)) => "Array".into(),
3663            // An array iterator and a Map/Set iterator carry the tags of their
3664            // prototypes (23.1.5.2.2, 24.1.5.2.2, 24.2.6.2.2).
3665            Some(JsObj::Iter { array: Some(_), .. }) => "Array Iterator".into(),
3666            Some(JsObj::Object(_)) if collection_iterator_view(h, v).is_some() => {
3667                match collection_iterator_view(h, v).map(|(b, _)| b) {
3668                    Some(b) if b.starts_with("Map") => "Map Iterator".into(),
3669                    _ => "Set Iterator".into(),
3670                }
3671            }
3672            // A lazy iterator helper brands as node does.
3673            Some(JsObj::Object(p))
3674                if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3675            {
3676                "Iterator Helper".into()
3677            }
3678            // A `DOMException` brands by its class, not as a plain `Error`.
3679            Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3680            // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3681            // `Object.prototype.toString.call(new Number(1))` is
3682            // `[object Number]` rather than `[object Object]`.
3683            Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3684                Value::Bool(_) => "Boolean".into(),
3685                Value::Int(_) | Value::Float(_) => "Number".into(),
3686                _ => "String".into(),
3687            },
3688            // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3689            // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3690            // {}))` is `'[object Array]'`. Everything else about a proxy brands
3691            // as a plain Object (a `Symbol.toStringTag` read through the `get`
3692            // trap is handled by the caller, before this).
3693            Some(JsObj::Proxy { target, .. }) => {
3694                let mut cur = target;
3695                for _ in 0..100 {
3696                    match h.get(cur) {
3697                        Some(JsObj::Proxy { target: t, .. }) => cur = t,
3698                        _ => break,
3699                    }
3700                }
3701                match h.get(cur) {
3702                    Some(JsObj::Array(_)) => "Array".into(),
3703                    _ => "Object".into(),
3704                }
3705            }
3706            // `function*` / `async function` / `async function*` carry their own
3707            // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3708            Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3709                Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3710                Some(d) if d.is_generator => "GeneratorFunction".into(),
3711                Some(d) if d.is_async => "AsyncFunction".into(),
3712                _ => "Function".into(),
3713            },
3714            // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3715            // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3716            Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3717                n.clone()
3718            }
3719            // A `<Ctor>.prototype` object brands as the constructor it belongs
3720            // to — `Object.prototype.toString.call(Set.prototype)` is
3721            // `[object Set]` — and a `require()`d module namespace is a plain
3722            // object. Neither is a function, so neither brands as one.
3723            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3724                match n.strip_suffix(".prototype") {
3725                    Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3726                    _ => "Object".into(),
3727                }
3728            }
3729            Some(JsObj::Class(_))
3730            | Some(JsObj::Builtin(_))
3731            | Some(JsObj::BoundFunc { .. })
3732            | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3733            // A suspended generator object is `[object Generator]`; an async one
3734            // `[object AsyncGenerator]`.
3735            Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3736            Some(JsObj::Generator { .. }) => "Generator".into(),
3737            Some(JsObj::RegExp(_)) => "RegExp".into(),
3738            Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3739            Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3740            Some(JsObj::Promise { .. }) => "Promise".into(),
3741            Some(JsObj::Symbol { .. }) => "Symbol".into(),
3742            Some(JsObj::BigInt(_)) => "BigInt".into(),
3743            // Native-tagged instances brand by their tag; a typed array brands by
3744            // its element kind (`@@kind`), and every Error subclass is `Error`.
3745            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3746                Some("TypedArray") => p
3747                    .get("@@kind")
3748                    .map(|k| h.str_of(k))
3749                    .unwrap_or_else(|| "Uint8Array".into()),
3750                Some("Buffer") => "Uint8Array".into(),
3751                // Every native class that really carries a `Symbol.toStringTag`
3752                // in Node brands by its own name. Verified against node v26:
3753                // `Object.prototype.toString.call(new WeakRef({}))` is
3754                // `[object WeakRef]`. The rest of the `@@native` tags
3755                // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3756                // classes with NO tag, so they stay `[object Object]` — listing
3757                // them here would invent a brand Node does not have.
3758                Some(
3759                    t @ ("ArrayBuffer"
3760                    | "DataView"
3761                    | "Date"
3762                    | "WeakRef"
3763                    | "FinalizationRegistry"
3764                    | "TextEncoder"
3765                    | "TextDecoder"
3766                    | "URL"
3767                    | "URLSearchParams"),
3768                ) => t.into(),
3769                _ if has_error_data(h, v) => "Error".into(),
3770                _ => "Object".into(),
3771            },
3772            _ => "Object".into(),
3773        },
3774        // node-js only produces the Value variants above; fusevm's shell-oriented
3775        // variants never arise here.
3776        _ => "Object".into(),
3777    };
3778    // Nothing about the value itself brands it. An ordinary object whose CHAIN
3779    // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3780    // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3781    // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3782    // `[object Map]` and was `[object Object]`.
3783    //
3784    // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3785    // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3786    // ACCESSOR returning the specific kind, so consulting the chain FIRST
3787    // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3788    // — three records caught it. `Error.prototype` carries no tag at all, so
3789    // inheriting from it borrows nothing.
3790    if tag == "Object" && !has_error_data(h, v) {
3791        if let Some(ctor) = chain_tag_ctor(h, v) {
3792            return ctor;
3793        }
3794    }
3795    tag
3796}
3797
3798fn b_setattr(vm: &mut VM, _: u8) -> Value {
3799    let val = vm.pop();
3800    let name = sval(&vm.pop());
3801    let recv = vm.pop();
3802    if let Err(e) = set_property(&recv, &name, val.clone()) {
3803        return abort(vm, e);
3804    }
3805    val
3806}
3807
3808/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3809/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3810/// `class C { static [k] = function(){} }`.
3811///
3812/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3813/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3814/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3815/// unconditionally here.
3816///
3817/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3818/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3819fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3820    let func = vm.pop();
3821    let kind = vm.pop().to_int();
3822    let key = vm.pop();
3823    let key = sval(&key);
3824    // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3825    // description gives the empty name, not `[undefined]`.
3826    let base = match with_host(|h| h.symbol_of_key(&key)) {
3827        Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3828            Some(JsObj::Symbol {
3829                desc: Some(desc), ..
3830            }) => format!("[{desc}]"),
3831            _ => String::new(),
3832        },
3833        None => key,
3834    };
3835    let name = match kind {
3836        host::member::GET => format!("get {base}"),
3837        host::member::SET => format!("set {base}"),
3838        _ => base,
3839    };
3840    with_host(|h| {
3841        let s = h.new_str(name);
3842        h.set_fn_prop(&func, "name", s);
3843    });
3844    func
3845}
3846
3847/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3848/// on the same path a plain `o.k = v` takes.
3849pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3850    set_property(recv, name, val)
3851}
3852
3853/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3854/// or `None` when it has none. Reads through a Proxy's
3855/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3856pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3857    let k = with_host(|h| h.new_str(key.to_string()));
3858    let d = own_descriptor_pub(obj, k).ok()?;
3859    if matches!(d, Value::Undef) {
3860        return None;
3861    }
3862    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3863    // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3864    // the host itself, so reading inside the closure double-borrows.
3865    let value = field("value");
3866    let writable = field("writable");
3867    let configurable = field("configurable");
3868    let truthy = |v: &Value| with_host(|h| h.truthy(v));
3869    let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3870    Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3871}
3872
3873/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3874/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3875/// proxy `set` trap forwarding to it both need.
3876///
3877/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3878/// with `receiver` as `this`; a data property does not write to `target` at all
3879/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3880/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3881/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3882/// documented way to forward a write, so the recursion hit every faithful
3883/// handler.
3884pub fn set_with_receiver(
3885    target: &Value,
3886    key: &str,
3887    val: Value,
3888    receiver: &Value,
3889) -> Result<bool, String> {
3890    // A proxy target answers through its own trap, which re-enters here with
3891    // whatever receiver the handler passes on.
3892    if crate::proxy::parts(target).is_some() {
3893        return crate::proxy::set(target, key, &val, receiver);
3894    }
3895    // An accessor anywhere on the target's chain wins, and sees `receiver`.
3896    if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3897        return match setter {
3898            Some(s) => {
3899                host::invoke(&s, vec![val], Some(receiver.clone()))?;
3900                Ok(true)
3901            }
3902            // A getter with no setter refuses the write rather than shadowing it.
3903            None => Ok(false),
3904        };
3905    }
3906    if !with_host(|h| h.can_write_prop(target, key)) {
3907        return Ok(false);
3908    }
3909    // Steps 3.b-3.d: only an object can receive the property, and its OWN
3910    // property decides — an accessor or a read-only slot refuses, and every
3911    // other case defines a plain data property.
3912    //
3913    // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3914    // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3915    // passed them through to `defineProperty`, which then threw `called on
3916    // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3917    if !with_host(|h| is_object_like(h, receiver)) {
3918        return Ok(false);
3919    }
3920    if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3921        if is_accessor || !writable {
3922            return Ok(false);
3923        }
3924    }
3925    // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3926    // by the receiver must not run, and a proxy receiver must reach its
3927    // `defineProperty` trap rather than its `set` trap.
3928    let desc = with_host(|h| {
3929        let mut m: IndexMap<String, Value> = IndexMap::new();
3930        m.insert("value".into(), val);
3931        m.insert("writable".into(), Value::Bool(true));
3932        m.insert("enumerable".into(), Value::Bool(true));
3933        m.insert("configurable".into(), Value::Bool(true));
3934        h.new_object(m)
3935    });
3936    if crate::proxy::parts(receiver).is_some() {
3937        return crate::proxy::define_property(receiver, key, &desc);
3938    }
3939    let k = with_host(|h| h.new_str(key.to_string()));
3940    define_property_pub(receiver, k, desc)?;
3941    Ok(true)
3942}
3943
3944/// Whether the first argument is a PRIMITIVE — including the three that ride as
3945/// heap handles, which a shape test misses.
3946fn is_primitive_arg(args: &[Value]) -> bool {
3947    let v = arg0(args);
3948    with_host(|h| host::is_primitive(h, &v))
3949}
3950
3951/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3952///
3953/// Adding a key to a non-extensible object reports differently from assigning
3954/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3955/// plain object, `[object Array]` for an array.
3956fn write_refused(recv: &Value, name: &str) -> String {
3957    let extensible = with_host(|h| h.is_extensible(recv));
3958    // Which of the two messages applies turns on whether the key already
3959    // EXISTS. Every shape that keeps its own properties in the fn-prop side
3960    // table answered a blanket `true` here, so adding a key to a frozen
3961    // function reported "read only" where node reports "not extensible".
3962    let has_own = with_host(|h| match h.get(recv) {
3963        Some(JsObj::Object(p)) => p.contains_key(name),
3964        Some(JsObj::Array(items)) => {
3965            name.parse::<usize>()
3966                .map(|i| i < items.len())
3967                .unwrap_or(false)
3968                || h.fn_prop(recv, name).is_some()
3969        }
3970        Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3971        _ => h.fn_prop(recv, name).is_some(),
3972    });
3973    if !extensible && !has_own {
3974        return host::type_error(&format!(
3975            "Cannot add property {name}, object is not extensible"
3976        ));
3977    }
3978    // The receiver renders the way every other brand-check message renders one
3979    // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3980    // class instance, `Error: m` for an error. Only Array was special-cased, so
3981    // every other exotic reported `#<Object>`.
3982    host::type_error(&format!(
3983        "Cannot assign to read only property '{name}' of object '{}'",
3984        no_side_effects_string(recv)
3985    ))
3986}
3987
3988fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3989    // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3990    // property of `undefined` or `null` throws, naming the key. Every such
3991    // write was silently discarded, so `u.x = 1` — the mirror of the single
3992    // most common runtime fault in JS, which the READ side already reports —
3993    // looked like it had succeeded.
3994    if with_host(|h| h.is_nullish(recv)) {
3995        return Err(host::type_error(&format!(
3996            "Cannot set properties of {} (setting '{name}')",
3997            with_host(|h| h.str_of(recv))
3998        )));
3999    }
4000    // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
4001    // throwaway wrapper — so it is discarded in sloppy code and throws in
4002    // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
4003    // `is_primitive` rather than a shape test: a string, a symbol and a bigint
4004    // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
4005    // value caught only numbers and booleans.
4006    if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
4007        return Err(host::type_error(&format!(
4008            "Cannot create property '{name}' on {} '{}'",
4009            with_host(|h| h.type_of(recv)),
4010            with_host(|h| h.str_of(recv))
4011        )));
4012    }
4013    // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
4014    // element. The class's own field initializers install theirs directly
4015    // (`host::init_one_field`), so a declaration never reaches this check.
4016    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
4017        return Err(private_brand_message(name, true));
4018    }
4019    // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
4020    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4021        // A `set` trap that returns falsish REFUSED the write: silent in sloppy
4022        // code, a TypeError in strict, exactly as an ordinary refused write is.
4023        if crate::proxy::set(recv, name, &val, recv)? {
4024            return Ok(());
4025        }
4026        if with_host(|h| h.current_strict()) {
4027            return Err(host::type_error(&format!(
4028                "'set' on proxy: trap returned falsish for property '{name}'"
4029            )));
4030        }
4031        return Ok(());
4032    }
4033    // A Proxy on the PROTOTYPE chain: `OrdinarySet` (10.1.9.2 step 2) hands a
4034    // key the receiver does not own to `parent.[[Set]](P, V, Receiver)`, so the
4035    // proxy's `set` trap runs with the ORIGINAL object as its receiver. The
4036    // write used to land straight on the receiver, and a trap inherited through
4037    // `Object.create(proxy)` never fired. A nearer link owning the key (data or
4038    // accessor) still wins, as `proxy_proto_link` checks.
4039    if name != "__proto__"
4040        && peek(recv, |o| match o {
4041            JsObj::Object(p) => Some(!p.contains_key(name)),
4042            _ => None,
4043        })
4044        .unwrap_or(false)
4045        && !with_host(|h| h.own_accessor(recv, name).is_some())
4046    {
4047        if let Some(link) = proxy_proto_link(recv, name) {
4048            if crate::proxy::set(&link, name, &val, recv)? {
4049                return Ok(());
4050            }
4051            if with_host(|h| h.current_strict()) {
4052                return Err(host::type_error(&format!(
4053                    "'set' on proxy: trap returned falsish for property '{name}'"
4054                )));
4055            }
4056            return Ok(());
4057        }
4058    }
4059    // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4060    // back. Writing only the own property left the two views disagreeing:
4061    // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4062    if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4063        with_host(|h| h.set_name(name, val.clone()));
4064    }
4065    // `obj.__proto__ = p` re-links the prototype — but only for the two values
4066    // the Annex B setter accepts, an Object or `null`. Everything else is a
4067    // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4068    // untouched and creates no own key), and a null-prototype object inherits
4069    // no such setter at all, so there the assignment is an ORDINARY own
4070    // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4071    // prototype to the number 5.
4072    if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4073        if with_host(|h| h.has_null_proto(recv)) {
4074            // falls through to the ordinary own-property write below
4075        } else {
4076            let assignable =
4077                with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4078            if assignable {
4079                // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4080                // NON-EXTENSIBLE object refuses — and unlike an ordinary
4081                // refused write, the setter throws in sloppy code too. It was
4082                // rewriting the link of a frozen object.
4083                if would_cycle(recv, &val) {
4084                    return Err(host::type_error("Cyclic __proto__ value"));
4085                }
4086                if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4087                    return Err(host::type_error(&format!(
4088                        "{} is not extensible",
4089                        no_side_effects_string(recv)
4090                    )));
4091                }
4092                with_host(|h| h.set_proto(recv, val));
4093            }
4094            return Ok(());
4095        }
4096    }
4097    // Every environment value is a STRING. `process.env.PORT = 8080` stores
4098    // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4099    // process; storing the number made it add instead.
4100    if !name.starts_with("@@")
4101        && with_host(
4102            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4103        )
4104    {
4105        let text = with_host(|h| h.str_of(&val));
4106        // Write THROUGH to the real environment as well. `process.env` is not a
4107        // private map: node applies the change to the process, so a child
4108        // spawned afterwards inherits it. Keeping it only in the JS object meant
4109        // `process.env.NODE_ENV = 'production'` was invisible to every
4110        // `spawnSync`/`execSync` that followed.
4111        std::env::set_var(name, &text);
4112        let sv = with_host(|h| h.new_str(text));
4113        with_host(|h| {
4114            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4115                p.insert(name.to_string(), sv);
4116            }
4117        });
4118        return Ok(());
4119    }
4120    // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4121    // no later read re-derives a header over the top of the assigned value.
4122    if name == "stack" {
4123        with_host(|h| {
4124            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4125                p.shift_remove("@@stackRaw");
4126            }
4127        });
4128    }
4129    // An inherited/own setter accessor intercepts the write. This is checked
4130    // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4131    // kind first: `writable` is a data-property attribute and means nothing on
4132    // an accessor, where the setter alone decides. Testing it first meant an
4133    // accessor defined through `Object.defineProperty` — which leaves
4134    // `writable` false, having no such field — silently swallowed every write
4135    // instead of calling its setter, so the standard clone idiom
4136    // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4137    // object whose setters did nothing. An accessor from an object literal
4138    // carries all-true attributes, which is why only the former broke.
4139    if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4140        if let Some(setter) = setter {
4141            let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4142            return Ok(());
4143        }
4144        // Only a getter: the write is refused — silent in sloppy mode, a
4145        // TypeError in strict code. The `return` above matters, since a
4146        // successful setter call must not fall into this.
4147        let _ = getter;
4148        if with_host(|h| h.current_strict()) {
4149            return Err(host::type_error(&format!(
4150                "Cannot set property {name} of #<Object> which has only a getter"
4151            )));
4152        }
4153        return Ok(());
4154    }
4155    // A non-writable property, or a new key on a non-extensible object, refuses
4156    // the write. In SLOPPY mode that is silent; in strict code it is a
4157    // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4158    // refusal used to be silent, so `'use strict'` did not catch a write to a
4159    // frozen object, which is most of the reason to freeze one.
4160    if !with_host(|h| h.can_write_prop(recv, name)) {
4161        if with_host(|h| h.current_strict()) {
4162            return Err(write_refused(recv, name));
4163        }
4164        return Ok(());
4165    }
4166    // Writing `name`/`prototype`/statics on a function value.
4167    if matches!(
4168        with_host(|h| h.kind_of(recv)),
4169        Some(ObjKind::Func) | Some(ObjKind::Class)
4170    ) {
4171        with_host(|h| h.set_fn_prop(recv, name, val));
4172        return Ok(());
4173    }
4174    // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4175    // Each bare reference is a fresh `Builtin` handle, so route to the stable
4176    // per-namespace side table rather than the per-index `fn_props`.
4177    if let Some(ns) = peek(recv, |o| match o {
4178        JsObj::Builtin(ns) => Some(ns.clone()),
4179        _ => None,
4180    }) {
4181        // `process.exitCode` is an accessor in Node, not a data property: the
4182        // setter validates and stores the code the process will finally exit
4183        // with. Landing it in the generic static table made it a write-only
4184        // decoration — `process.exitCode = 3` read back as 3 and the process
4185        // still exited 0.
4186        if ns == "process" && name == "exitCode" {
4187            return crate::stdlib::process::set_exit_code(&val);
4188        }
4189        with_host(|h| h.set_builtin_static(&ns, name, val));
4190        return Ok(());
4191    }
4192    // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4193    // `String.prototype`, `TypeError.prototype`) is mirrored into the
4194    // per-namespace side table as well as the object's own map. Instances are
4195    // not linked to these objects by `proto_of` — the chain walk never reaches
4196    // them — so the mirror is what makes `String.prototype.pad = f` visible as
4197    // `"x".pad`. The own-map write below still happens, so reading the
4198    // prototype itself and enumerating it keep working unchanged.
4199    if let Some(ns) = with_host(|h| {
4200        h.intrinsic_proto_ctor(recv)
4201            .map(str::to_string)
4202            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4203    }) {
4204        with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4205    }
4206    // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4207    // writability check above already refused it on a FROZEN regexp, which it
4208    // could only do once `integrity_keys` learned that `lastIndex` is an own
4209    // property.
4210    if name == "lastIndex" {
4211        if let Some(n) = with_host(|h| match h.get(recv) {
4212            Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4213            _ => None,
4214        }) {
4215            with_host(|h| {
4216                if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4217                    r.last_index = if n.is_finite() && n >= 0.0 {
4218                        crate::utf16::U16Index::new(n as usize)
4219                    } else {
4220                        crate::utf16::U16Index::ZERO
4221                    };
4222                }
4223            });
4224            return Ok(());
4225        }
4226    }
4227    // An `arguments` object is an ORDINARY object with a `length` data property,
4228    // not an array: a write PAST the end adds an index and leaves `length`
4229    // alone. The array backing grew it instead, so `f(1)` followed by
4230    // `arguments[1] = 9` reported `arguments.length` as 2.
4231    if let Ok(i) = name.parse::<usize>() {
4232        if is_arguments(recv) && i >= array_len(recv) {
4233            with_host(|h| h.set_fn_prop(recv, name, val));
4234            return Ok(());
4235        }
4236    }
4237    // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4238    if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4239        let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4240        if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4241            return Ok(());
4242        }
4243        // An index write to a view over a DETACHED buffer is DROPPED. Falling
4244        // through would store it as an ordinary own property, which then showed
4245        // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4246        if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4247            return Ok(());
4248        }
4249        // `buf[i] = n` writes through to the Buffer's hidden byte array.
4250        if crate::stdlib::buffer::byte_set(recv, name, &val) {
4251            return Ok(());
4252        }
4253    }
4254    // Any own property on an exotic with no property map of its own. This sits
4255    // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4256    // moves its match cursor rather than being shadowed by a side-table entry.
4257    if uses_side_table(recv) {
4258        with_host(|h| h.set_fn_prop(recv, name, val));
4259        return Ok(());
4260    }
4261    // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4262    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4263        && name != "length"
4264        && name.parse::<usize>().is_err()
4265    {
4266        with_host(|h| h.set_fn_prop(recv, name, val));
4267        return Ok(());
4268    }
4269    // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4270    // and does so outside the host borrow because `ToNumber` may run a user
4271    // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4272    let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4273        let want = host::to_array_length(&val)?;
4274        // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4275        // STOPS at the first element that cannot be deleted, leaving the length
4276        // just past it. Truncating regardless discarded a non-configurable
4277        // element and reported a length node would not have accepted.
4278        let floor = with_host(|h| {
4279            let old = match h.get(recv) {
4280                Some(JsObj::Array(items)) => items.len(),
4281                _ => 0,
4282            };
4283            let mut stop = want;
4284            for i in (want..old).rev() {
4285                if !h.prop_attrs(recv, &i.to_string()).configurable {
4286                    stop = i + 1;
4287                    break;
4288                }
4289            }
4290            stop
4291        });
4292        Some(floor.max(want))
4293    } else {
4294        None
4295    };
4296    with_host(|h| match h.get_mut(recv) {
4297        Some(JsObj::Object(props)) => {
4298            // Adding a *new* array-index key must re-place it into ascending
4299            // integer-key order (updating an existing key keeps its position).
4300            let is_new = !props.contains_key(name);
4301            props.insert(name.to_string(), val);
4302            if is_new && host::array_index(name).is_some() {
4303                host::canonicalize_own_keys(props);
4304            }
4305        }
4306        Some(JsObj::Array(items)) => {
4307            if let Some(n) = new_len {
4308                // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4309                // just the one own key); shrinking drops any hole past the end.
4310                let old = items.len();
4311                items.resize(n, Value::Undef);
4312                if n > old {
4313                    h.mark_hole_range(recv, old..n);
4314                } else {
4315                    h.truncate_holes(recv, n);
4316                }
4317            } else if let Ok(i) = name.parse::<usize>() {
4318                // A write PAST the end leaves the skipped positions elided.
4319                let old = items.len();
4320                if i >= old {
4321                    items.resize(i + 1, Value::Undef);
4322                }
4323                items[i] = val;
4324                if i > old {
4325                    h.mark_hole_range(recv, old..i);
4326                }
4327                // …and the written index itself is no longer one. This is the
4328                // single site that keeps a hole record from outliving the
4329                // elision it describes: every array element write in the
4330                // language reaches it.
4331                h.clear_hole(recv, i);
4332            }
4333        }
4334        _ => {}
4335    });
4336    Ok(())
4337}
4338
4339fn b_getitem(vm: &mut VM, _: u8) -> Value {
4340    let idx = vm.pop();
4341    let recv = vm.pop();
4342    let key = match host::to_property_key(&idx) {
4343        Ok(k) => k,
4344        Err(e) => return abort(vm, e),
4345    };
4346    match get_property(&recv, &key) {
4347        Ok(v) => v,
4348        Err(e) => abort(vm, e),
4349    }
4350}
4351
4352fn b_setitem(vm: &mut VM, _: u8) -> Value {
4353    let val = vm.pop();
4354    let idx = vm.pop();
4355    let recv = vm.pop();
4356    let key = match host::to_property_key(&idx) {
4357        Ok(k) => k,
4358        Err(e) => return abort(vm, e),
4359    };
4360    if let Err(e) = set_property(&recv, &key, val.clone()) {
4361        return abort(vm, e);
4362    }
4363    val
4364}
4365
4366/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4367/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4368/// the three cannot drift. Reports `false` for a non-configurable property
4369/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4370/// which is also what deleting an absent key reports.
4371pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4372    // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4373    // `delete u.x` reported success instead.
4374    if with_host(|h| h.is_nullish(recv)) {
4375        return Err(host::type_error(
4376            "Cannot convert undefined or null to object",
4377        ));
4378    }
4379    // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4380    // throw — the reason this reports a `Result` rather than a bare `bool`.
4381    if let Some(b) = crate::proxy::delete(recv, key)? {
4382        return Ok(b);
4383    }
4384    // `delete globalThis.x` removes a global a script created. It lives in the
4385    // globals map, not the object's property map, so the ordinary path reported
4386    // success and removed nothing — the binding stayed readable afterwards.
4387    if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4388        return Ok(true);
4389    }
4390    // `delete require.cache[id]` drops the module so the next `require` of that
4391    // file runs it again — the whole point of exposing the cache.
4392    if peek(recv, |o| match o {
4393        JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4394        _ => None,
4395    }) == Some(true)
4396    {
4397        return Ok(crate::module::cache_delete(key));
4398    }
4399    // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4400    // JS view, so a child spawned afterwards no longer sees it.
4401    if !key.starts_with("@@")
4402        && with_host(
4403            |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4404        )
4405    {
4406        std::env::remove_var(key);
4407    }
4408    // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4409    // `Object.prototype`) is non-configurable when it is a constant or a
4410    // constructor's `prototype`, and `delete` of one answers false without
4411    // removing anything. There is no property map behind a namespace, so the
4412    // ordinary attribute lookup below cannot tell — it reported success for
4413    // every one of them.
4414    // A REAL intrinsic prototype object carries the write in its own map AND in
4415    // the side table the instance read consults, so the delete has to clear
4416    // both. Clearing only the map left `Object.prototype.patch` deleted as far
4417    // as the prototype was concerned and still inherited by every object.
4418    if let Some(ns) = with_host(|h| {
4419        h.intrinsic_proto_ctor(recv)
4420            .map(str::to_string)
4421            .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4422    }) {
4423        with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4424    }
4425    if let Some(ns) = peek(recv, |o| match o {
4426        JsObj::Builtin(ns) => Some(ns.clone()),
4427        _ => None,
4428    }) {
4429        // A script-assigned static is an ordinary configurable property and is
4430        // removed from the side table the assignment landed in. Falling through
4431        // to the attribute check below answered true and deleted nothing, so a
4432        // patch survived its own `delete`.
4433        if with_host(|h| h.remove_builtin_static(&ns, key)) {
4434            return Ok(true);
4435        }
4436        if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4437            return Ok(false);
4438        }
4439    }
4440    if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4441        return Ok(false);
4442    }
4443    // An accessor lives in its own table, not the property map, so removing it
4444    // has to be explicit — otherwise `delete` reported success while the getter
4445    // kept answering and `in` kept reporting the key.
4446    if with_host(|h| h.own_accessor(recv, key).is_some()) {
4447        with_host(|h| h.remove_accessor(recv, key));
4448        return Ok(true);
4449    }
4450    with_host(|h| {
4451        let index = key.parse::<usize>();
4452        match h.get_mut(recv) {
4453            Some(JsObj::Object(props)) => {
4454                props.shift_remove(key);
4455                return;
4456            }
4457            Some(JsObj::Array(items)) => {
4458                if let Ok(i) = index {
4459                    if i < items.len() {
4460                        // `delete a[i]` punches a HOLE: the length is unchanged
4461                        // but the index stops being an own property.
4462                        items[i] = Value::Undef;
4463                        h.mark_hole(recv, i);
4464                    }
4465                    return;
4466                }
4467            }
4468            _ => {}
4469        }
4470        // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4471        // a function/class, is an ordinary own property kept in the side table.
4472        h.remove_fn_prop(recv, key);
4473    });
4474    Ok(true)
4475}
4476
4477fn b_delitem(vm: &mut VM, _: u8) -> Value {
4478    let strict = vm.pop();
4479    let idx = vm.pop();
4480    let recv = vm.pop();
4481    // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4482    // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4483    // description and delete a key nothing ever wrote.
4484    let key = match host::to_property_key(&idx) {
4485        Ok(k) => k,
4486        Err(e) => return abort(vm, e),
4487    };
4488    match delete_property(&recv, &key) {
4489        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4490            abort(vm, refused_delete_error(&recv, &key))
4491        }
4492        Ok(b) => Value::Bool(b),
4493        Err(e) => abort(vm, e),
4494    }
4495}
4496
4497fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4498    let strict = vm.pop();
4499    let name = sval(&vm.pop());
4500    let recv = vm.pop();
4501    match delete_property(&recv, &name) {
4502        Ok(false) if with_host(|h| h.truthy(&strict)) => {
4503            abort(vm, refused_delete_error(&recv, &name))
4504        }
4505        Ok(b) => Value::Bool(b),
4506        Err(e) => abort(vm, e),
4507    }
4508}
4509
4510/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4511/// receiver renders the way every other brand-check message renders one.
4512fn refused_delete_error(recv: &Value, key: &str) -> String {
4513    // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4514    // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4515    // here rather than in the shared `[[Delete]]`.
4516    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4517        return host::type_error(&format!(
4518            "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4519        ));
4520    }
4521    // A non-callable builtin NAMESPACE renders as a plain object here — node
4522    // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4523    let shown = match peek(recv, |o| match o {
4524        JsObj::Builtin(ns) => Some(ns.clone()),
4525        _ => None,
4526    }) {
4527        Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4528        _ => no_side_effects_string(recv),
4529    };
4530    host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4531}
4532
4533// ── constructors ──────────────────────────────────────────────────────────────
4534
4535fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4536    let parts = pop_n(vm, argc as usize);
4537    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4538    with_host(|h| h.new_str(s))
4539}
4540
4541fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4542    let items = pop_n(vm, argc as usize);
4543    with_host(|h| h.new_array(items))
4544}
4545
4546/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4547/// only for an array literal that actually contains an elision, so a dense
4548/// literal costs nothing. Returns `undefined`; the array stays on the stack
4549/// underneath (the compiler `Dup`s it).
4550fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4551    let idx = vm.pop();
4552    let arr = vm.pop();
4553    let i = match idx {
4554        Value::Int(i) if i >= 0 => i as usize,
4555        _ => return Value::Undef,
4556    };
4557    with_host(|h| h.mark_hole(&arr, i));
4558    Value::Undef
4559}
4560
4561fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4562    let flat = pop_n(vm, argc as usize);
4563    let mut props: IndexMap<String, Value> = IndexMap::new();
4564    // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4565    let mut proto_override: Option<Value> = None;
4566    let mut method_keys: Vec<String> = Vec::new();
4567    let mut i = 0;
4568    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4569        if i + 2 >= flat.len() {
4570            break;
4571        }
4572        // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4573        // the literal reserves its slot here with the `@@ord:` marker key that
4574        // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4575        // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4576        // marker landed at the end.
4577        if matches!(flat[i], Value::Int(2)) {
4578            let key = with_host(|h| h.str_of(&flat[i + 1]));
4579            props
4580                .entry(format!("{}{key}", host::ORD_MARKER))
4581                .or_insert(Value::Undef);
4582            i += 3;
4583            continue;
4584        }
4585        // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4586        // recorded so the literal can become its `[[HomeObject]]` below.
4587        if matches!(flat[i], Value::Int(3)) {
4588            let key = with_host(|h| h.str_of(&flat[i + 1]));
4589            method_keys.push(key.clone());
4590            props.insert(key, flat[i + 2].clone());
4591            i += 3;
4592            continue;
4593        }
4594        let spread = matches!(flat[i], Value::Int(1));
4595        if spread {
4596            let src = flat[i + 1].clone();
4597            // A STRING source spreads its index properties (`{..."ab"}` is
4598            // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4599            // String exotic object owns one enumerable property per UTF-16 code
4600            // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4601            // a string source contributed nothing and `{..."ab"}` was `{}`.
4602            // Every other primitive (number/boolean/symbol) boxes to an object
4603            // with no own enumerable properties, and null/undefined are ignored,
4604            // so those correctly stay no-ops on the path below.
4605            if let Some(s) = with_host(|h| h.as_str(&src)) {
4606                for idx in 0..crate::utf16::len(&s) {
4607                    if let Ok(ch) = get_property(&src, &idx.to_string()) {
4608                        props.insert(idx.to_string(), ch);
4609                    }
4610                }
4611                i += 3;
4612                continue;
4613            }
4614            // Object spread copies own *enumerable* properties only — never the
4615            // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4616            // into something that still claimed to be a Buffer) and never a
4617            // property a descriptor marked non-enumerable.
4618            // A getter that throws during spread propagates as a thrown value,
4619            // which in the VM means aborting the frame.
4620            let entries = match host::own_enum_entries_deep(&src) {
4621                Ok(e) => e,
4622                Err(e) => return abort(vm, e),
4623            };
4624            for (k, v) in entries {
4625                props.insert(k, v);
4626            }
4627            // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4628            // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4629            for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4630                props.insert(k, v);
4631            }
4632        } else {
4633            let key = with_host(|h| h.str_of(&flat[i + 1]));
4634            if key == "__proto__" {
4635                proto_override = Some(flat[i + 2].clone());
4636            } else {
4637                props.insert(key, flat[i + 2].clone());
4638            }
4639        }
4640        i += 3;
4641    }
4642    with_host(|h| {
4643        let o = h.new_object(props);
4644        if let Some(p) = proto_override {
4645            if matches!(p, Value::Obj(_)) {
4646                h.set_proto(&o, p);
4647            }
4648        }
4649        // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4650        // is what `super` inside it resolves through. The home object is fixed
4651        // at definition, so a method that merely arrives as a value
4652        // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4653        // method-valued property instead rebound the original and changed what
4654        // IT resolved.
4655        for key in &method_keys {
4656            let m = match h.get(&o) {
4657                Some(JsObj::Object(p)) => p.get(key).cloned(),
4658                _ => None,
4659            };
4660            if let Some(m) = m {
4661                if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4662                    f.home_object = Some(o.clone());
4663                }
4664            }
4665        }
4666        o
4667    })
4668}
4669
4670fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4671    let def_id = match vm.pop() {
4672        Value::Int(n) => n as usize,
4673        Value::Float(f) => f as usize,
4674        _ => return abort(vm, "internal: MKFUNC id".into()),
4675    };
4676    let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4677        Some(d) => (
4678            d.is_arrow,
4679            (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4680        ),
4681        None => (false, None),
4682    });
4683    with_host(|h| {
4684        let mut env = h.current_env_capture();
4685        let this = h.current_this();
4686        // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4687        // exactly as it uses the enclosing `this`. Nothing was captured, so
4688        // `super.m()` inside an arrow reported the method missing — in a class
4689        // method as well as an object literal.
4690        let (home_class, home_static, home_object) = if is_arrow {
4691            h.current_home()
4692        } else {
4693            (None, false, None)
4694        };
4695        // A named function expression closes over an extra scope holding its own
4696        // name, so the body can recurse through it (`function f(){ … f() … }`)
4697        // independently of whatever the outer binding is later set to.
4698        if self_name.is_some() {
4699            env = host::child_env(env);
4700        }
4701        let f = h.alloc(JsObj::Func(FuncVal {
4702            def_id,
4703            env: Some(env.clone()),
4704            this,
4705            is_arrow,
4706            home_class,
4707            home_static,
4708            home_object,
4709        }));
4710        if let Some(n) = self_name {
4711            env.borrow_mut().vars.insert(n, f.clone());
4712        }
4713        f
4714    })
4715}
4716
4717// ── truthiness / coercion / equality ──────────────────────────────────────────
4718
4719fn b_truthy(vm: &mut VM, _: u8) -> Value {
4720    let v = vm.pop();
4721    Value::Bool(with_host(|h| h.truthy(&v)))
4722}
4723
4724fn b_nullish(vm: &mut VM, _: u8) -> Value {
4725    let v = vm.pop();
4726    Value::Bool(with_host(|h| h.is_nullish(&v)))
4727}
4728
4729fn b_tostr(vm: &mut VM, _: u8) -> Value {
4730    let v = vm.pop();
4731    // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4732    // `String(x)`, object keys).
4733    match host::to_string_value(&v) {
4734        Ok(s) => s,
4735        Err(e) => abort(vm, e),
4736    }
4737}
4738
4739fn b_typeof(vm: &mut VM, _: u8) -> Value {
4740    let v = vm.pop();
4741    with_host(|h| {
4742        let t = h.type_of(&v);
4743        h.new_str(t)
4744    })
4745}
4746
4747/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4748/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4749fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4750    let name = sval(&vm.pop());
4751    // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4752    // for an UNBOUND name, but a `let` above its declaration is bound and
4753    // throws. Reading the marker's type answered "function".
4754    if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4755        return abort(vm, host::tdz_error(&name));
4756    }
4757    if let Some(v) = with_host(|h| h.read_name(&name)) {
4758        if with_host(|h| h.is_tdz(&v)) {
4759            return abort(vm, host::tdz_error(&name));
4760        }
4761    }
4762    // Bound name (user variable) → typeof its value.
4763    if let Some(v) = with_host(|h| h.read_name(&name)) {
4764        return with_host(|h| {
4765            let t = h.type_of(&v);
4766            h.new_str(t)
4767        });
4768    }
4769    // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4770    // would produce, then take its type (so object-namespaces like `console`/
4771    // `Math`/`JSON`/`process` report "object", constructors report "function").
4772    let t = match name.as_str() {
4773        "undefined" => "undefined".to_string(),
4774        "NaN" | "Infinity" => "number".to_string(),
4775        "globalThis" | "global" => "object".to_string(),
4776        n if is_namespace(n) || is_known_builtin(n) => {
4777            let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4778            with_host(|h| h.type_of(&v)).to_string()
4779        }
4780        _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4781    };
4782    with_host(|h| h.new_str(t))
4783}
4784
4785fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4786    let b = vm.pop();
4787    let a = vm.pop();
4788    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4789}
4790
4791fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4792    let b = vm.pop();
4793    let a = vm.pop();
4794    // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4795    // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4796    // runs before the host borrow. Object ⇄ object stays a reference check.
4797    let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4798        (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4799            Ok(p) => (p, b),
4800            Err(e) => return abort(vm, e),
4801        },
4802        (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4803            Ok(p) => (a, p),
4804            Err(e) => return abort(vm, e),
4805        },
4806        _ => (a, b),
4807    };
4808    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4809}
4810
4811fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4812    let ctor = vm.pop();
4813    let obj = vm.pop();
4814    match host::instance_of(&obj, &ctor) {
4815        Ok(b) => Value::Bool(b),
4816        Err(e) => abort(vm, e),
4817    }
4818}
4819
4820// ── bitwise / unary ───────────────────────────────────────────────────────────
4821
4822fn b_binop(vm: &mut VM, _: u8) -> Value {
4823    let b = vm.pop();
4824    let a = vm.pop();
4825    let tag = match vm.pop() {
4826        Value::Int(n) => n,
4827        _ => 0,
4828    };
4829    // Both operands are ToPrimitive-d with the number hint before ToInt32
4830    // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4831    let r = host::to_primitive(&a, "number")
4832        .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4833        .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4834    finish(vm, r)
4835}
4836
4837fn b_unary(vm: &mut VM, _: u8) -> Value {
4838    let v = vm.pop();
4839    let tag = match vm.pop() {
4840        Value::Int(n) => n,
4841        _ => 0,
4842    };
4843    // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4844    // computed in arbitrary precision.
4845    if with_host(|h| h.is_bigint_val(&v)) {
4846        return match tag {
4847            host::unop::POS => abort(
4848                vm,
4849                host::type_error("Cannot convert a BigInt value to a number"),
4850            ),
4851            host::unop::BITNOT => {
4852                let b = with_host(|h| h.as_bigint(&v)).unwrap();
4853                let r = -(b + num_bigint::BigInt::from(1));
4854                with_host(|h| h.new_bigint(r))
4855            }
4856            _ => Value::Undef,
4857        };
4858    }
4859    // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4860    // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4861    let n = match host::to_number_value(&v) {
4862        Ok(n) => n,
4863        Err(e) => return abort(vm, e),
4864    };
4865    match tag {
4866        host::unop::POS => Value::Float(n),
4867        host::unop::BITNOT => {
4868            let i = if n.is_finite() {
4869                n.trunc() as i64 as i32
4870            } else {
4871                0
4872            };
4873            Value::Float(!i as f64)
4874        }
4875        _ => Value::Undef,
4876    }
4877}
4878
4879// ── membership ────────────────────────────────────────────────────────────────
4880
4881fn b_contains(vm: &mut VM, _: u8) -> Value {
4882    let container = vm.pop();
4883    let key = vm.pop();
4884    // `x in y` requires y to be an object. V8 names both operands:
4885    // `Cannot use 'in' operator to search for 'a' in 5`.
4886    // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4887    // `Value::Obj` in this host but is not an object, so the shape test alone
4888    // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4889    // where node throws. `is_primitive` is the same predicate `ToObject` and
4890    // `typeof` use, so the three cannot disagree about what an object is.
4891    if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4892        let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4893        return abort(
4894            vm,
4895            host::type_error(&format!(
4896                "Cannot use 'in' operator to search for '{k}' in {c}"
4897            )),
4898        );
4899    }
4900    let k = match host::to_property_key(&key) {
4901        Ok(k) => k,
4902        Err(e) => return abort(vm, e),
4903    };
4904    match has_property(&container, &k) {
4905        Ok(b) => Value::Bool(b),
4906        Err(e) => abort(vm, e),
4907    }
4908}
4909
4910// ── control ───────────────────────────────────────────────────────────────────
4911
4912fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4913    let v = vm.pop();
4914    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4915    vm.ip = vm.chunk.ops.len();
4916    v
4917}
4918
4919/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4920/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4921/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4922fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4923    let label = sval(&vm.pop());
4924    let label = (!label.is_empty()).then_some(label);
4925    with_host(|h| h.signal = Some(host::Signal::Break(label)));
4926    vm.ip = vm.chunk.ops.len();
4927    Value::Undef
4928}
4929
4930/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4931fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4932    let label = sval(&vm.pop());
4933    let label = (!label.is_empty()).then_some(label);
4934    with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4935    vm.ip = vm.chunk.ops.len();
4936    Value::Undef
4937}
4938
4939/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4940/// describes what the `try` is nested in (see [`host::unwind`]):
4941///
4942/// * no signal → `NONE`, execution continues normally;
4943/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4944///   keeps travelling outward;
4945/// * `break`/`continue` targeting the enclosing loop → consume it and report
4946///   `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4947///   continue target;
4948/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4949///   the signal pending, so leaving this loop re-dispatches it one level out.
4950fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4951    let cont_tag = sval(&vm.pop());
4952    let brk_tag = sval(&vm.pop());
4953    let sig = match with_host(|h| h.signal.clone()) {
4954        Some(s) => s,
4955        None => return Value::Int(host::unwind::NONE),
4956    };
4957    // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4958    let propagate = |vm: &mut VM| {
4959        vm.ip = vm.chunk.ops.len();
4960        Value::Int(host::unwind::NONE)
4961    };
4962    match &sig {
4963        host::Signal::Return(_) => propagate(vm),
4964        host::Signal::Break(label) => {
4965            if brk_tag == host::unwind::NO_LOOP {
4966                return propagate(vm);
4967            }
4968            let mine = match label {
4969                None => true, // unlabeled: always the innermost enclosing context
4970                Some(l) => brk_tag == *l,
4971            };
4972            if mine {
4973                with_host(|h| h.signal = None);
4974            }
4975            // Not ours: still leave this context by its break exit, keeping the
4976            // signal pending for the next dispatch point one level out.
4977            Value::Int(host::unwind::BREAK)
4978        }
4979        host::Signal::Continue(label) => {
4980            let mine = match label {
4981                // Unlabeled `continue` binds to the innermost continue-catching
4982                // loop — which a `switch` between here and it is NOT.
4983                None => cont_tag != host::unwind::NO_LOOP,
4984                Some(l) => cont_tag == *l,
4985            };
4986            if mine {
4987                with_host(|h| h.signal = None);
4988                return Value::Int(host::unwind::CONTINUE);
4989            }
4990            if brk_tag == host::unwind::NO_LOOP {
4991                return propagate(vm);
4992            }
4993            // The target loop is further out: exit the innermost context here and
4994            // re-dispatch there.
4995            Value::Int(host::unwind::BREAK)
4996        }
4997    }
4998}
4999
5000fn b_throw(vm: &mut VM, _: u8) -> Value {
5001    let v = vm.pop();
5002    let msg = with_host(|h| {
5003        h.exc = Some(v.clone());
5004        // Prefer an error object's message for the top-level report.
5005        error_display(h, &v)
5006    });
5007    abort(vm, msg)
5008}
5009
5010fn error_display(h: &host::JsHost, v: &Value) -> String {
5011    if let Some(JsObj::Object(props)) = h.get(v) {
5012        let name = props
5013            .get("name")
5014            .map(|x| h.str_of(x))
5015            .unwrap_or_else(|| "Error".into());
5016        if let Some(m) = props.get("message") {
5017            return format!("Uncaught {name}: {}", h.str_of(m));
5018        }
5019    }
5020    format!("Uncaught {}", h.str_of(v))
5021}
5022
5023fn b_try(vm: &mut VM, _: u8) -> Value {
5024    let id = match vm.pop() {
5025        Value::Int(n) => n as usize,
5026        _ => return abort(vm, "internal: TRY id".into()),
5027    };
5028    // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
5029    // its handler and its finalizer bytecode — every time control entered it,
5030    // which for a `try` inside a loop is once per iteration.
5031    let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
5032        Some(t) => t,
5033        None => return abort(vm, "internal: unknown try id".into()),
5034    };
5035    let mut pending: Option<String> = None;
5036    // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
5037    // through can leave block scopes open. Snapshot the scope and restore it
5038    // before the handler and after the whole statement.
5039    let scope = with_host(|h| h.scope_snapshot());
5040
5041    with_host(|h| h.push_scope()); // the try block is its own block scope
5042    let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
5043        with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
5044    });
5045    with_host(|h| h.restore_scope(scope.clone()));
5046    let signal_after = with_host(|h| h.signal.is_some());
5047    if let Err(e) = body_res {
5048        if signal_after {
5049            pending = Some(e);
5050        } else if has_handler {
5051            // Bind the thrown value (or a synthesized error) to the catch param.
5052            let thrown =
5053                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5054            with_host(|h| {
5055                h.error = None;
5056                h.exc = None;
5057            });
5058            // The catch parameter is block-scoped to the handler.
5059            with_host(|h| h.push_scope());
5060            if let Some(name) = &catch_bind {
5061                with_host(|h| h.declare_name(name, thrown));
5062            }
5063            let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5064                with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5065            });
5066            with_host(|h| h.restore_scope(scope.clone()));
5067            if let Err(e2) = hres {
5068                pending = Some(e2);
5069            }
5070        } else {
5071            pending = Some(e);
5072        }
5073    }
5074
5075    // finally always runs; a finally error/signal supersedes.
5076    if has_finalizer {
5077        let sig_before = with_host(|h| h.signal.take());
5078        with_host(|h| h.push_scope()); // ditto for `finally`
5079        let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5080            with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5081        });
5082        with_host(|h| h.restore_scope(scope.clone()));
5083        match fres {
5084            Ok(_) => {
5085                if with_host(|h| h.signal.is_none()) {
5086                    // The finalizer completed normally: the try/catch block's own
5087                    // abrupt completion resumes.
5088                    with_host(|h| h.signal = sig_before);
5089                } else {
5090                    // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5091                    // completion is abrupt (`return`/`break`/`continue` inside
5092                    // `finally`), that completion REPLACES the try/catch block's —
5093                    // including a pending throw, which is discarded, not rethrown.
5094                    pending = None;
5095                    with_host(|h| {
5096                        h.error = None;
5097                        h.exc = None;
5098                    });
5099                }
5100            }
5101            Err(e) => pending = Some(e),
5102        }
5103    }
5104
5105    if let Some(e) = pending {
5106        return abort(vm, e);
5107    }
5108    Value::Undef
5109}
5110
5111/// Synthesize an `Error`-shaped object from an internal error string, linked to
5112/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5113pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5114    h.ensure_error_protos();
5115    // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5116    // it is not one of the ECMAScript error classes below.
5117    if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5118        if let Some((name, msg)) = rest.split_once('\u{1}') {
5119            return dom_exception_with(h, name, msg);
5120        }
5121    }
5122    // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5123    // error class, exactly as Node's internal errors render it in `.stack`.
5124    let (head, rest) = match e.split_once(": ") {
5125        Some((n, m)) => (n, m.to_string()),
5126        None => ("", e.to_string()),
5127    };
5128    let (base, code) = match head.split_once(" [") {
5129        Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5130        _ => (head, None),
5131    };
5132    let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5133        (base.to_string(), rest)
5134    } else {
5135        ("Error".to_string(), e.to_string())
5136    };
5137    // A `host::plain_coded_error` marker: the code rides at the head of the
5138    // MESSAGE rather than in the class, because Node's native-layer errors set
5139    // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5140    // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5141    // internal and must never reach a user-visible `.message`.
5142    let mut code = code;
5143    // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5144    // bracketed head, false for the marker form.
5145    let mut bracketed = code.is_some();
5146    // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5147    let mut fields: Vec<(String, String)> = Vec::new();
5148    if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5149        if let Some((c, m)) = rest.split_once('\u{1}') {
5150            code = Some(c.to_string());
5151            bracketed = false;
5152            let (m, fs) = host::split_error_fields(m);
5153            fields = fs
5154                .into_iter()
5155                .map(|(k, v)| (k.to_string(), v.to_string()))
5156                .collect();
5157            message = m.to_string();
5158        }
5159    }
5160    let mut props: IndexMap<String, Value> = IndexMap::new();
5161    let mv = h.new_str(message.clone());
5162    props.insert("message".into(), mv);
5163    if let Some(c) = &code {
5164        let cv = h.new_str(c.clone());
5165        props.insert("code".into(), cv);
5166        for (k, v) in fields {
5167            let fv = h.new_str(v);
5168            props.insert(k, fv);
5169        }
5170        if bracketed {
5171            // Marks this as a Node JS-layer error, whose `toString` brackets the
5172            // code. A native-layer error has the same `.code` and does not.
5173            props.insert("@@nodeError".into(), Value::Bool(true));
5174        }
5175    }
5176    let label = match (&code, bracketed) {
5177        (Some(c), true) => format!("{name} [{c}]"),
5178        _ => name.clone(),
5179    };
5180    let frames = h.stack_frames();
5181    let stack = if message.is_empty() {
5182        format!("{label}{frames}")
5183    } else {
5184        format!("{label}: {message}{frames}")
5185    };
5186    let sv = h.new_str(stack);
5187    props.insert("stack".into(), sv);
5188    // A libuv system-error message is itself the canonical encoding of the
5189    // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5190    // filesystem/network failure recovers the enumerable `code`/`errno`/
5191    // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5192    // single most common error-handling idiom in Node packages) depend on.
5193    for (k, v) in syscall_error_fields(&message) {
5194        let sv = match v {
5195            SysField::Str(s) => h.new_str(s),
5196            SysField::Num(n) => Value::Float(n),
5197        };
5198        props.insert(k.into(), sv);
5199    }
5200    let obj = h.new_object(props);
5201    if let Some(p) = host::error_proto_of(h, &name) {
5202        h.set_proto(&obj, p);
5203    }
5204    // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5205    // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5206    h.hide_prop(&obj, "message");
5207    h.hide_prop(&obj, "stack");
5208    obj
5209}
5210
5211enum SysField {
5212    Str(String),
5213    Num(f64),
5214}
5215
5216/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5217/// own properties Node hangs off a system error. Returns empty for any message
5218/// that is not in that shape.
5219fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5220    let (code, rest) = match message.split_once(": ") {
5221        Some((c, r))
5222            if c.len() >= 2
5223                && c.starts_with('E')
5224                && c.bytes()
5225                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5226        {
5227            (c, r)
5228        }
5229        _ => return Vec::new(),
5230    };
5231    let mut out: Vec<(&'static str, SysField)> = vec![
5232        ("errno", SysField::Num(errno_for(code))),
5233        ("code", SysField::Str(code.to_string())),
5234    ];
5235    // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5236    if let Some((_, tail)) = rest.split_once(", ") {
5237        let (syscall, path) = match tail.split_once(" '") {
5238            // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5239            // one, so the scan stops at its closing quote rather than at the
5240            // end of the line — which had been swallowing `' -> 'dest` into the
5241            // path for every `rename` and `copyFile` failure.
5242            Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5243            None => (tail, None),
5244        };
5245        out.push(("syscall", SysField::Str(syscall.to_string())));
5246        if let Some(p) = path {
5247            out.push(("path", SysField::Str(p.to_string())));
5248        }
5249    }
5250    out
5251}
5252
5253/// The negative `errno` Node reports for a libuv error code on this platform.
5254/// Only the codes `err_str` can produce are mapped; anything else reports the
5255/// generic `EIO` number rather than inventing a value.
5256fn errno_for(code: &str) -> f64 {
5257    let n: i32 = match code {
5258        "ENOENT" => 2,
5259        "EACCES" => 13,
5260        "EEXIST" => 17,
5261        "ENOTDIR" => 20,
5262        "EISDIR" => 21,
5263        "EINVAL" => 22,
5264        "EPIPE" => 32,
5265        "ENOTEMPTY" => 66,
5266        _ => 5, // EIO
5267    };
5268    -f64::from(n)
5269}
5270
5271// ── iteration ─────────────────────────────────────────────────────────────────
5272
5273fn b_getiter(vm: &mut VM, _: u8) -> Value {
5274    let v = vm.pop();
5275    // A generator is its own iterator (resumed lazily by FORITER).
5276    if with_host(|h| h.is_generator_val(&v)) {
5277        return v;
5278    }
5279    // A Proxy's iterator comes from its traps, materialized eagerly: the
5280    // `lookup_chain` probe below reads the property map a proxy does not have.
5281    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5282        return match crate::proxy::iterate(&v) {
5283            Ok(Some(items)) => with_host(|h| {
5284                h.alloc(JsObj::Iter {
5285                    items,
5286                    idx: 0,
5287                    array: None,
5288                })
5289            }),
5290            Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5291            Err(e) => abort(vm, e),
5292        };
5293    }
5294    // Arrays and strings take the direct path below: they have no iterator
5295    // state to preserve and are the hot case, so they must not pay a property
5296    // lookup and a call per loop.
5297    let direct = matches!(
5298        with_host(|h| h.kind_of(&v)),
5299        Some(ObjKind::Array) | Some(ObjKind::Str)
5300    );
5301    // …but only while their `Symbol.iterator` is still reachable. It comes from
5302    // the intrinsic prototype, so replacing the link takes it away: node reports
5303    // `a is not iterable` for an array whose prototype is a plain object, where
5304    // the fast path below iterated the backing vector regardless.
5305    if !own_intrinsic_reachable(&v)
5306        && !matches!(
5307            get_property(&v, "@@iterator"),
5308            Ok(ref f) if with_host(|h| host::is_callable(h, f))
5309        )
5310    {
5311        let shown = with_host(|h| h.inspect(&v));
5312        let msg = host::type_error(&format!("{shown} is not iterable"));
5313        return abort(vm, host::name_call_site(vm, &shown, msg));
5314    }
5315    // Anything else with a `Symbol.iterator`: call it for the iterator object.
5316    //
5317    // Resolved as a full property READ, not a stored-property lookup. A
5318    // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5319    // dispatches its methods through the stdlib table rather than a property
5320    // map, so a `lookup_chain` probe found nothing and the loop fell through to
5321    // materializing the value — which threw for `URLSearchParams` and
5322    // snapshotted for `Map`. Spreading the same object already worked, because
5323    // that path had been fixed and this one had not.
5324    if !direct {
5325        if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5326            if with_host(|h| host::is_callable(h, &iter_fn)) {
5327                return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5328                    Ok(it) => it,
5329                    Err(e) => abort(vm, e),
5330                };
5331            }
5332        }
5333    }
5334    // An array is iterated live, as its `values()` iterator does: a snapshot
5335    // missed every push during the loop, so a worklist `for (const n of q)
5336    // q.push(…)` stopped after the first element.
5337    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Array) {
5338        return array_iterator(&v, host::ArrayIterKind::Values);
5339    }
5340    match with_host(|h| h.iter_vec(&v)) {
5341        Ok(items) => with_host(|h| {
5342            h.alloc(JsObj::Iter {
5343                items,
5344                idx: 0,
5345                array: None,
5346            })
5347        }),
5348        // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5349        // reports `a is not iterable`. The text was recorded for this op.
5350        Err(e) => {
5351            let shown = with_host(|h| h.inspect(&v));
5352            let named = host::name_call_site(vm, &shown, e);
5353            abort(vm, named)
5354        }
5355    }
5356}
5357
5358fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5359    let v = vm.pop();
5360    // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5361    // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5362    // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5363    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5364        // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5365        // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5366        // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5367        // the body the way node's do, instead of all firing up front.
5368        return match crate::proxy::own_keys(&v) {
5369            Ok(keys) => with_host(|h| {
5370                let out: Vec<Value> = keys
5371                    .unwrap_or_default()
5372                    .into_iter()
5373                    .filter(|k| !host::is_symbol_key(k))
5374                    .map(|k| h.new_str(k))
5375                    .collect();
5376                h.new_array(out)
5377            }),
5378            Err(e) => abort(vm, e),
5379        };
5380    }
5381    let mut keys = with_host(|h| h.enum_keys(&v));
5382    // A member patched onto the receiver's INTRINSIC prototype is enumerable
5383    // and inherited, so `for-in` visits it after the own keys — but the
5384    // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5385    // pass never reaches them.
5386    if !with_host(|h| h.has_null_proto(&v)) {
5387        let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5388        for ns in intrinsic_proto_namespaces(&v) {
5389            for k in with_host(|h| h.builtin_static_keys(&ns)) {
5390                if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5391                    keys.push(with_host(|h| h.new_str(k)));
5392                }
5393            }
5394        }
5395    }
5396    with_host(|h| h.new_array(keys))
5397}
5398
5399/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5400/// own constructor's and then `Object`'s, the same two steps
5401/// `inherited_builtin_static` looks a value up in.
5402fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5403    let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5404        Some(c) => Some(c),
5405        None if is_arguments(v) => Some("Object"),
5406        None => with_host(|h| default_ctor_name(h, v)),
5407    };
5408    let mut out: Vec<String> = ctor
5409        .filter(|c| *c != "Object")
5410        .map(|c| format!("{c}.prototype"))
5411        .into_iter()
5412        .collect();
5413    out.push("Object.prototype".to_string());
5414    out
5415}
5416
5417/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5418///
5419/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5420/// of the enumerable keys is observationally the same for everything except
5421/// this), and the body can delete a key before the loop reaches it. Node does
5422/// not visit a key deleted that way; without this check `delete d.z` inside the
5423/// loop still produced `x,y,z`.
5424///
5425/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5426/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5427/// `has` trap, which node never fires for `for-in`. That also puts each trap
5428/// call immediately before its visit, matching node's interleaving — the trap
5429/// log used to show every `gopd` up front because the key list was filtered
5430/// eagerly.
5431fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5432    let key = vm.pop();
5433    let obj = vm.pop();
5434    let name = with_host(|h| h.str_of(&key));
5435    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5436        return match crate::proxy::own_enumerable(&obj, &name) {
5437            Ok(b) => Value::Bool(b),
5438            Err(e) => abort(vm, e),
5439        };
5440    }
5441    // A STRING's keys are its character indices. `in` is not defined on a string
5442    // primitive at all, so the ordinary path below has no answer for one and
5443    // `for (const i in 'abc')` came back empty.
5444    if let Some(s) = with_host(|h| h.as_str(&obj)) {
5445        let len = crate::utf16::len(&s);
5446        return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5447    }
5448    // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5449    // there and does NOT re-check enumerability — making one non-enumerable
5450    // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5451    // (The Proxy branch above does re-check, because there the answer comes from
5452    // the trap node itself calls.)
5453    Value::Bool(has_property_ordinary(&obj, &name))
5454}
5455
5456fn b_foriter(vm: &mut VM, _: u8) -> Value {
5457    let it = match vm.stack.last() {
5458        Some(v) => v.clone(),
5459        None => return abort(vm, "internal: FORITER with empty stack".into()),
5460    };
5461    // A built-in iterator: a snapshot (strings, a Proxy's items) or live over
5462    // an array.
5463    if let Some(step) = iter_step(&it) {
5464        return match step {
5465            Some(v) => {
5466                vm.push(v);
5467                Value::Bool(true)
5468            }
5469            None => Value::Bool(false),
5470        };
5471    }
5472    // Generator: resume one step.
5473    if with_host(|h| h.is_generator_val(&it)) {
5474        return match host::gen_resume(&it, Value::Undef) {
5475            Ok(host::GenStep::Yield(v)) => {
5476                vm.push(v);
5477                Value::Bool(true)
5478            }
5479            Ok(host::GenStep::Done(_)) => Value::Bool(false),
5480            Err(e) => abort(vm, e),
5481        };
5482    }
5483    // A user iterator object with a `.next()` returning `{ value, done }`.
5484    match host::call_method(&it, "next", Vec::new()) {
5485        Ok(step) => {
5486            let done = get_property(&step, "done")
5487                .map(|d| with_host(|h| h.truthy(&d)))
5488                .unwrap_or(true);
5489            if done {
5490                Value::Bool(false)
5491            } else {
5492                match get_property(&step, "value") {
5493                    Ok(v) => {
5494                        vm.push(v);
5495                        Value::Bool(true)
5496                    }
5497                    Err(e) => abort(vm, e),
5498                }
5499            }
5500        }
5501        Err(e) => abort(vm, e),
5502    }
5503}
5504
5505fn b_unpack(vm: &mut VM, _: u8) -> Value {
5506    let star = match vm.pop() {
5507        Value::Int(n) => n,
5508        _ => -1,
5509    };
5510    let count = match vm.pop() {
5511        Value::Int(n) => n as usize,
5512        _ => 0,
5513    };
5514    let iterable = vm.pop();
5515    // Without a `...rest` element the pattern needs exactly `count` values and
5516    // must then close the iterator; draining hung on an unbounded source.
5517    let items = match if star < 0 {
5518        host::iter_take(&iterable, count)
5519    } else {
5520        host::iter_all(&iterable)
5521    } {
5522        Ok(v) => v,
5523        // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5524        // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5525        // was recorded for this op at compile time.
5526        Err(e) => {
5527            // Node names the source only when the pattern's right-hand side is
5528            // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5529            // Anything else (a member, a call, a nested pattern, a parameter)
5530            // reports the TYPE instead, with the property note. Measured across
5531            // twelve shapes rather than guessed.
5532            let msg = match host::call_site_text(vm) {
5533                Some(text) => host::type_error(&format!("{text} is not iterable")),
5534                None if e.ends_with(" is not iterable") => {
5535                    host::type_error(&not_iterable_typed(&iterable))
5536                }
5537                None => e,
5538            };
5539            return abort(vm, msg);
5540        }
5541    };
5542    let ordered: Vec<Value> = if star < 0 {
5543        (0..count)
5544            .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5545            .collect()
5546    } else {
5547        let si = star as usize;
5548        let after = count.saturating_sub(si + 1);
5549        let rest_end = items.len().saturating_sub(after).max(si);
5550        let mut out: Vec<Value> = Vec::with_capacity(count);
5551        for i in 0..si {
5552            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5553        }
5554        let rest: Vec<Value> = items
5555            .get(si..rest_end)
5556            .map(|s| s.to_vec())
5557            .unwrap_or_default();
5558        out.push(with_host(|h| h.new_array(rest)));
5559        for j in 0..after {
5560            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5561        }
5562        out
5563    };
5564    if ordered.is_empty() {
5565        return Value::Undef;
5566    }
5567    for it in ordered[1..].iter().rev().cloned() {
5568        vm.push(it);
5569    }
5570    ordered[0].clone()
5571}
5572
5573fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5574    let flat = pop_n(vm, argc as usize);
5575    let mut out = Vec::new();
5576    // Elided positions of an array literal (tag 2), recorded as the run-time
5577    // index each lands on — which only this walk knows, because a preceding
5578    // spread contributes an unknown number of elements. Call-argument lists,
5579    // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5580    // empty for them.
5581    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5582    let mut i = 0;
5583    while i + 1 < flat.len() {
5584        let val = flat[i + 1].clone();
5585        match flat[i] {
5586            // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5587            // report a non-iterable differently, which is the only reason the
5588            // two are told apart here.
5589            Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5590                let shown = with_host(|h| h.inspect(&val));
5591                host::name_call_site(vm, &shown, e)
5592            }) {
5593                Ok(items) => out.extend(items),
5594                Err(e) => return abort(vm, e),
5595            },
5596            Value::Int(3) => match host::iter_all(&val) {
5597                Ok(items) => out.extend(items),
5598                Err(e) => {
5599                    // A NULLISH spread names the value and what could not be
5600                    // read off it; anything else names the missing protocol.
5601                    let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5602                    return abort(
5603                        vm,
5604                        match shown {
5605                            Some(s) => host::type_error(&format!(
5606                                "{s} is not iterable (cannot read property {s})"
5607                            )),
5608                            None if e.ends_with(" is not iterable") => host::type_error(
5609                                "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5610                            ),
5611                            None => e,
5612                        },
5613                    );
5614                }
5615            },
5616            Value::Int(2) => {
5617                holes.insert(out.len());
5618                out.push(Value::Undef);
5619            }
5620            _ => out.push(val),
5621        }
5622        i += 2;
5623    }
5624    with_host(|h| {
5625        let arr = h.new_array(out);
5626        h.install_holes(&arr, holes);
5627        arr
5628    })
5629}
5630
5631// ── calls ──────────────────────────────────────────────────────────────────────
5632
5633fn b_call(vm: &mut VM, argc: u8) -> Value {
5634    let mut args = pop_n(vm, argc as usize);
5635    let name = sval(&args.remove(0));
5636    let r = host::call_named(&name, args);
5637    // A bare name that resolved to a non-callable reports the VALUE
5638    // (`undefined is not a function`); node names the identifier. Resolving it
5639    // again to learn what the message said costs nothing off the error path.
5640    let r = r.map_err(|e| {
5641        let shown = global_binding(&name)
5642            .map(|v| with_host(|h| h.str_of(&v)))
5643            .unwrap_or_default();
5644        host::name_call_site(vm, &shown, e)
5645    });
5646    finish(vm, r)
5647}
5648
5649/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5650/// method name. `call_method` resolves by name and bottoms out in
5651/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5652/// that happens to be a function reported "is not a function". Read the element
5653/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5654/// A computed call's key is a property key, so it goes through ToPropertyKey:
5655/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5656/// answers "" for a number, which turned `arr[0]()` into a call to the method
5657/// named "" — so the key is stringified here instead.
5658fn call_key_of(v: &Value) -> String {
5659    if let Value::Str(s) = v {
5660        return (**s).clone();
5661    }
5662    // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5663    // spelling that `str_of` does not produce — it renders
5664    // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5665    // method by that display text and reported it was not a function, for every
5666    // object including a plain literal with a computed symbol method. Reading
5667    // the same property without calling it worked, which is what hid this.
5668    with_host(|h| h.property_key(v))
5669}
5670
5671fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5672    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5673        return None;
5674    }
5675    let f = get_property(recv, name).ok()?;
5676    with_host(|h| host::is_callable(h, &f))
5677        .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5678}
5679
5680fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5681    let mut args = pop_n(vm, argc as usize);
5682    let recv = args.remove(0);
5683    let name = call_key_of(&args.remove(0));
5684    if let Some(r) = index_element_call(&recv, &name, &args) {
5685        return finish(vm, r);
5686    }
5687    let r = host::call_method(&recv, &name, args);
5688    // `z.f()` on a missing method is `z.f is not a function` in node, not
5689    // `f is not a function`: V8 names the callee as the source wrote it. The
5690    // text was recorded for this op at compile time.
5691    let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5692    finish(vm, r)
5693}
5694
5695fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5696    let mut args = pop_n(vm, argc as usize);
5697    let callable = args.remove(0);
5698    let r = host::invoke(&callable, args, None);
5699    // The callee here is an expression, not a name, so the message it produced
5700    // describes the VALUE (`undefined is not a function`); node names the
5701    // expression. Same site table, keyed on that rendering.
5702    let r = r.map_err(|e| {
5703        let shown = with_host(|h| h.str_of(&callable));
5704        host::name_call_site(vm, &shown, e)
5705    });
5706    finish(vm, r)
5707}
5708
5709/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5710/// rather than a fixed count of stack slots.
5711///
5712/// `compile_new` used to compile each argument with `compile_expr`, and a
5713/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5714/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5715/// Invalid Date.
5716fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5717    let args_arr = vm.pop();
5718    let ctor = vm.pop();
5719    let args = host::iter_all(&args_arr).unwrap_or_default();
5720    let r = host::construct(&ctor, args).map_err(|e| {
5721        let shown = with_host(|h| h.str_of(&ctor));
5722        host::name_call_site(vm, &shown, e)
5723    });
5724    finish(vm, r)
5725}
5726
5727fn b_new(vm: &mut VM, argc: u8) -> Value {
5728    let mut args = pop_n(vm, argc as usize);
5729    let ctor = args.remove(0);
5730    let r = host::construct(&ctor, args);
5731    // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5732    // call does.
5733    let r = r.map_err(|e| {
5734        let shown = with_host(|h| h.str_of(&ctor));
5735        host::name_call_site(vm, &shown, e)
5736    });
5737    finish(vm, r)
5738}
5739
5740fn b_apply(vm: &mut VM, _: u8) -> Value {
5741    let args_arr = vm.pop();
5742    let callable = vm.pop();
5743    let args = host::iter_all(&args_arr).unwrap_or_default();
5744    let r = host::invoke(&callable, args, None);
5745    finish(vm, r)
5746}
5747
5748fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5749    let args_arr = vm.pop();
5750    let name = call_key_of(&vm.pop());
5751    let recv = vm.pop();
5752    let args = host::iter_all(&args_arr).unwrap_or_default();
5753    if let Some(r) = index_element_call(&recv, &name, &args) {
5754        return finish(vm, r);
5755    }
5756    let r = host::call_method(&recv, &name, args);
5757    finish(vm, r)
5758}
5759
5760// ── numeric hook ──────────────────────────────────────────────────────────────
5761
5762/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5763/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5764///
5765/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5766/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5767/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5768/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5769/// the VM and `arith` runs under the host's `RefCell` borrow.
5770pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5771    use NumOp::*;
5772    let (a, b) = match op {
5773        // `==`/`!=` only convert when the OTHER side is a primitive that can be
5774        // compared numerically or textually; `{} == {}` stays a reference check.
5775        Eq | Ne => {
5776            let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5777            match (pa, pb) {
5778                (false, true) if coerces_against_object(b) => {
5779                    (host::to_primitive(a, "default")?, b.clone())
5780                }
5781                (true, false) if coerces_against_object(a) => {
5782                    (a.clone(), host::to_primitive(b, "default")?)
5783                }
5784                _ => (a.clone(), b.clone()),
5785            }
5786        }
5787        // `+` uses the default hint (`valueOf` first, but a string result still
5788        // selects concatenation); everything else uses the number hint.
5789        Add => (
5790            host::to_primitive(a, "default")?,
5791            host::to_primitive(b, "default")?,
5792        ),
5793        _ => (
5794            host::to_primitive(a, "number")?,
5795            host::to_primitive(b, "number")?,
5796        ),
5797    };
5798    reject_symbol_operand(op, &a, &b)?;
5799    with_host(|h| h.arith(op, &a, &b))
5800}
5801
5802/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5803/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5804/// concatenated `Symbol(desc)` into the result.
5805///
5806/// Which of the two messages V8 uses is decided by whether the operation is
5807/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5808/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5809/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5810/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5811/// (`Symbol() == 1` is `false`), so they are left alone.
5812fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5813    use NumOp::*;
5814    if matches!(op, Eq | Ne) {
5815        return Ok(());
5816    }
5817    let (sym, concat) = with_host(|h| {
5818        let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5819        let is_str =
5820            |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5821        (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5822    });
5823    if !sym {
5824        return Ok(());
5825    }
5826    Err(host::type_error(if matches!(op, Add) && concat {
5827        "Cannot convert a Symbol value to a string"
5828    } else {
5829        "Cannot convert a Symbol value to a number"
5830    }))
5831}
5832
5833/// Whether a primitive `v` makes `==` against an object convert that object
5834/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5835/// `undefined` and booleans are settled without a `ToPrimitive` call
5836/// (a boolean is coerced to a number first, and then it does).
5837fn coerces_against_object(v: &Value) -> bool {
5838    match v {
5839        Value::Undef => false,
5840        Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5841        _ => with_host(|h| !h.is_null(v)),
5842    }
5843}
5844
5845// ══ standard library ═══════════════════════════════════════════════════════════
5846
5847/// Namespaces reachable as bare globals.
5848fn is_namespace(name: &str) -> bool {
5849    matches!(
5850        name,
5851        "console"
5852            | "Math"
5853            | "JSON"
5854            | "Object"
5855            | "Array"
5856            | "Number"
5857            | "String"
5858            | "Boolean"
5859            | "Symbol"
5860            | "Reflect"
5861            | "Promise"
5862            | "process"
5863            | "Buffer"
5864            | "URL"
5865            | "URLSearchParams"
5866    )
5867}
5868
5869const GLOBAL_FUNCS: &[&str] = &[
5870    "parseInt",
5871    "parseFloat",
5872    "isNaN",
5873    "isFinite",
5874    "encodeURIComponent",
5875    "decodeURIComponent",
5876    "encodeURI",
5877    "decodeURI",
5878    // Annex B legacy encoders. Still globals on every engine, and still called
5879    // by pre-`encodeURIComponent` library code.
5880    "escape",
5881    "unescape",
5882    "eval",
5883    "String",
5884    "Number",
5885    "Boolean",
5886    "Array",
5887    "Object",
5888    "Function",
5889    "Symbol",
5890    "Map",
5891    "Set",
5892    "WeakMap",
5893    "WeakSet",
5894    "Promise",
5895    "Error",
5896    "TypeError",
5897    "RangeError",
5898    "SyntaxError",
5899    "ReferenceError",
5900    "EvalError",
5901    "URIError",
5902    "AggregateError",
5903    "DOMException",
5904    "Iterator",
5905    "BigInt",
5906    "RegExp",
5907    "Date",
5908    "ArrayBuffer",
5909    "DataView",
5910    "Uint8Array",
5911    "Int8Array",
5912    "Uint8ClampedArray",
5913    "Int16Array",
5914    "Uint16Array",
5915    "Int32Array",
5916    "Uint32Array",
5917    "Float32Array",
5918    "Float64Array",
5919    "BigInt64Array",
5920    "BigUint64Array",
5921    "WeakRef",
5922    "FinalizationRegistry",
5923    "TextEncoder",
5924    "TextDecoder",
5925    // WHATWG Fetch globals (see `stdlib::fetch`).
5926    "fetch",
5927    "Headers",
5928    "Request",
5929    "Response",
5930    "Blob",
5931    "File",
5932    "FormData",
5933    "AbortController",
5934    "AbortSignal",
5935    "queueMicrotask",
5936    "setTimeout",
5937    "setInterval",
5938    "setImmediate",
5939    "clearTimeout",
5940    "clearInterval",
5941    "clearImmediate",
5942    "structuredClone",
5943    // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5944    // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5945    "btoa",
5946    "atob",
5947    "Proxy",
5948    "require",
5949    // CommonJS loader dispatch targets referenced by per-module `require`
5950    // closures (see `module.rs`); never written by user code.
5951    "__cjs_require",
5952    "__cjs_resolve",
5953    "__cjs_cache",
5954];
5955
5956const NS_METHODS: &[&str] = &[
5957    "console.log",
5958    "console.error",
5959    "console.warn",
5960    "console.info",
5961    "console.debug",
5962    "Math.abs",
5963    "Math.acos",
5964    "Math.acosh",
5965    "Math.asin",
5966    "Math.asinh",
5967    "Math.atan",
5968    "Math.atanh",
5969    "Math.atan2",
5970    "Math.ceil",
5971    "Math.cbrt",
5972    "Math.expm1",
5973    "Math.clz32",
5974    "Math.cos",
5975    "Math.cosh",
5976    "Math.exp",
5977    "Math.floor",
5978    "Math.fround",
5979    "Math.hypot",
5980    "Math.imul",
5981    "Math.log",
5982    "Math.log1p",
5983    "Math.log2",
5984    "Math.log10",
5985    "Math.max",
5986    "Math.min",
5987    "Math.pow",
5988    "Math.random",
5989    "Math.round",
5990    "Math.sign",
5991    "Math.sin",
5992    "Math.sinh",
5993    "Math.sqrt",
5994    "Math.tan",
5995    "Math.tanh",
5996    "Math.trunc",
5997    "JSON.stringify",
5998    "JSON.parse",
5999    "JSON.rawJSON",
6000    "JSON.isRawJSON",
6001    "Object.keys",
6002    "Object.values",
6003    "Object.entries",
6004    "Object.assign",
6005    "Object.freeze",
6006    "Object.is",
6007    "Object.fromEntries",
6008    "Object.getPrototypeOf",
6009    "Object.setPrototypeOf",
6010    "Object.create",
6011    "Object.getOwnPropertyNames",
6012    "Object.getOwnPropertySymbols",
6013    "Object.defineProperty",
6014    "Object.getOwnPropertyDescriptor",
6015    "Object.getOwnPropertyDescriptors",
6016    "Object.defineProperties",
6017    "Object.isFrozen",
6018    "Object.isSealed",
6019    "Object.seal",
6020    "Object.preventExtensions",
6021    "Object.isExtensible",
6022    "Object.hasOwn",
6023    "Object.groupBy",
6024    "Array.isArray",
6025    "Array.from",
6026    "Array.fromAsync",
6027    "Array.of",
6028    "Number.isFinite",
6029    "Number.isInteger",
6030    "Number.isNaN",
6031    "Number.isSafeInteger",
6032    "Number.parseFloat",
6033    "Number.parseInt",
6034    "String.fromCharCode",
6035    "String.fromCodePoint",
6036    "String.raw",
6037    "Symbol.for",
6038    "Symbol.keyFor",
6039    "BigInt.asIntN",
6040    "BigInt.asUintN",
6041    "Proxy.revocable",
6042    "Reflect.defineProperty",
6043    "Reflect.deleteProperty",
6044    "Reflect.apply",
6045    "Reflect.construct",
6046    "Reflect.get",
6047    "Reflect.getOwnPropertyDescriptor",
6048    "Reflect.getPrototypeOf",
6049    "Reflect.has",
6050    "Reflect.isExtensible",
6051    "Reflect.ownKeys",
6052    "Reflect.preventExtensions",
6053    "Reflect.set",
6054    "Reflect.setPrototypeOf",
6055    "Promise.resolve",
6056    "Promise.reject",
6057    "Promise.all",
6058    "Promise.allSettled",
6059    "Promise.race",
6060    "Promise.any",
6061    "Promise.withResolvers",
6062    "Promise.try",
6063    "RegExp.escape",
6064    "Error.isError",
6065    "Map.groupBy",
6066    "Response.json",
6067    "Response.error",
6068    "Response.redirect",
6069    "AbortSignal.abort",
6070    "AbortSignal.timeout",
6071    "process.nextTick",
6072    "Error.captureStackTrace",
6073    "require.resolve",
6074    "require.resolve.paths",
6075    "process.memoryUsage.rss",
6076];
6077
6078/// The `name` and `length` a builtin function reports, from the generated
6079/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6080/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6081/// and the core-module functions, whose arity is not specified anywhere.
6082pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6083    crate::arity::BUILTIN_ARITY
6084        .binary_search_by(|(k, _, _)| (*k).cmp(key))
6085        .ok()
6086        .map(|i| {
6087            let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6088            (name, len)
6089        })
6090}
6091
6092/// The `name` a builtin function reports. The table answers for an intrinsic;
6093/// anything else falls back to the last segment of the key, which is what the
6094/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6095/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6096/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6097pub fn builtin_name(key: &str) -> &str {
6098    if let Some((name, _)) = builtin_meta(key) {
6099        return name;
6100    }
6101    match key.strip_prefix("@proto:") {
6102        Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6103        // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6104        // with a `get` prefix), which is what `util.inspect` prints for it and
6105        // what a library reads to identify one.
6106        None => key.rsplit('.').next().unwrap_or(key),
6107    }
6108}
6109
6110/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6111/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6112/// `"get size"` it has to build.
6113pub fn proto_getter_name(key: &str) -> Option<String> {
6114    let (verb, rest) = match key.strip_prefix("@protoget:") {
6115        Some(rest) => ("get", rest),
6116        None => ("set", key.strip_prefix("@protoset:")?),
6117    };
6118    let (_, member) = rest.split_once(':')?;
6119    Some(format!("{verb} {member}"))
6120}
6121
6122pub fn is_known_builtin(name: &str) -> bool {
6123    // Binary search over a sorted INDEX of the two tables rather than a scan of
6124    // both. This runs on every call whose callee is a builtin — `call_method`
6125    // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6126    // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6127    // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6128    // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6129    // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6130    // now. The index is built once per process and derived FROM those tables, so
6131    // it cannot drift from them.
6132    //
6133    // That is an operation count, not a measured time, and NO wall-clock win is
6134    // claimed. Re-measured in isolation (this hunk alone applied to the previous
6135    // commit, interleaved against it, minimums over ten rounds each): the A/B
6136    // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6137    // the A/A control — the SAME binary under both labels — came out 1.084,
6138    // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6139    // this machine the change is not distinguishable from noise. It is kept for
6140    // the comparison count and because it cannot drift from the tables it is
6141    // derived from, not because anything got faster.
6142    static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6143    let sorted = SORTED.get_or_init(|| {
6144        let mut v: Vec<&'static str> = GLOBAL_FUNCS
6145            .iter()
6146            .chain(NS_METHODS.iter())
6147            .copied()
6148            .collect();
6149        v.sort_unstable();
6150        v
6151    });
6152    sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6153}
6154
6155// ── dynamic functions (runtime source → callable) ────────────────────────────
6156
6157/// Build a callable from a complete function-expression source text — the ONE
6158/// dynamic-function generator on this frontend.
6159///
6160/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6161/// wrapping parentheses needed to parse it as an expression: those are added
6162/// here, and `src` itself is retained so `Function.prototype.toString` reports
6163/// what V8 reports. The two callers synthesize different text and both shapes
6164/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6165///
6166/// The body runs in the MODULE scope, never the constructing function's scope
6167/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6168/// *global* environment). That also makes a `var` inside the body a function
6169/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6170/// returns 6 and leaves `globalThis.zz` `undefined`.
6171pub fn dynamic_function(src: &str) -> Result<Value, String> {
6172    let f = crate::eval_in_global_scope(&format!("({src})"))?;
6173    with_host(|h| {
6174        let s = h.new_str(src.to_string());
6175        h.set_fn_prop(&f, "@@source", s);
6176    });
6177    Ok(f)
6178}
6179
6180/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6181///
6182/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6183/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6184/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6185/// arguments at all, both the parameter list and the body are empty.
6186///
6187/// Measured on node v26.7.0:
6188///
6189/// ```text
6190/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6191/// new Function().toString()                     === 'function anonymous(\n) {\n\n}'
6192/// new Function('a,b','c','return [a,b,c]').length === 3
6193/// new Function('a','b','return a+b').name       === 'anonymous'
6194/// ```
6195pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6196    let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6197    let (params, body) = match parts.split_last() {
6198        Some((body, params)) => (params.join(","), body.clone()),
6199        None => (String::new(), String::new()),
6200    };
6201    dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6202}
6203
6204/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6205/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6206/// other route to the same function value is an INDIRECT eval and evaluates in
6207/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6208/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6209/// do not.
6210///
6211/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6212pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6213    let v = arg.cloned().unwrap_or(Value::Undef);
6214    let is_string =
6215        matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6216    if !is_string {
6217        return Ok(v);
6218    }
6219    let src = with_host(|h| h.str_of(&v));
6220    // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6221    // decides both the early errors the COMPILE raises and the variable
6222    // environment below. An INDIRECT one is global-scope sloppy code.
6223    let caller_strict = direct && with_host(|h| h.current_strict());
6224    let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6225    if !direct {
6226        return host::run_chunk_in_global_scope(chunk);
6227    }
6228    // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6229    // so its `var`s and function declarations die with it. Only a SLOPPY one
6230    // shares the caller's, which is the form that can inject a binding — and
6231    // sharing it unconditionally meant `eval('var x=1')` inside strict code
6232    // left `x` behind.
6233    let strict = caller_strict
6234        || src.trim_start().starts_with("'use strict'")
6235        || src.trim_start().starts_with("\"use strict\"");
6236    if !strict {
6237        // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6238        // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6239        // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6240        // or `class` declared inside therefore dies with the eval; every one of
6241        // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6242        // behind and `let a=1; eval('let a=2')` overwrote it.
6243        //
6244        // `push_scope` is exactly that split: `var` and a hoisted function
6245        // declaration bind to `base_env`, which this does not touch.
6246        with_host(|h| h.push_scope());
6247        let out = host::run_chunk_on(chunk);
6248        with_host(|h| h.pop_scope());
6249        return out;
6250    }
6251    let prev = with_host(|h| h.push_var_scope());
6252    let out = host::run_chunk_on(chunk);
6253    with_host(|h| h.pop_var_scope(prev));
6254    out
6255}
6256
6257/// Call a resolved builtin function (global or `namespace.method`).
6258pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6259    // `require(spec)`: the ENTRY script's top-level require — core module first,
6260    // else the CommonJS loader resolving from the entry file's directory.
6261    if name == "require" {
6262        let spec = with_host(|h| h.str_of(&arg0(&args)));
6263        return crate::module::require(&spec, &crate::module::entry_dir());
6264    }
6265    // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6266    // into the loader, resolving `spec` against the module's own directory.
6267    if name == "__cjs_require" {
6268        let spec = with_host(|h| h.str_of(&arg0(&args)));
6269        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6270        return crate::module::require(&spec, std::path::Path::new(&from));
6271    }
6272    if name == "process.memoryUsage.rss" {
6273        return Ok(crate::stdlib::process::memory_usage_rss());
6274    }
6275    if name == "require.resolve.paths" {
6276        let spec = with_host(|h| h.str_of(&arg0(&args)));
6277        // A core module is not looked up on disk at all.
6278        if crate::stdlib::is_core(&spec) {
6279            return Ok(with_host(|h| h.null()));
6280        }
6281        let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6282        return Ok(with_host(|h| {
6283            let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6284            h.new_array(items)
6285        }));
6286    }
6287    // A `require.extensions` entry. This runtime's loader does not dispatch
6288    // through the map, so calling one is the loader's own behaviour for that
6289    // extension rather than a hook point.
6290    if let Some(ext) = name.strip_prefix("@@extension:") {
6291        let _ = ext;
6292        return Ok(Value::Undef);
6293    }
6294    // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6295    if name == "require.resolve" {
6296        let spec = with_host(|h| h.str_of(&arg0(&args)));
6297        if crate::stdlib::is_core(&spec) {
6298            return Ok(with_host(|h| h.new_str(spec)));
6299        }
6300        return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6301            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6302            None => Err(crate::host::plain_coded_error(
6303                "Error",
6304                "MODULE_NOT_FOUND",
6305                &format!("Cannot find module '{spec}'"),
6306            )),
6307        };
6308    }
6309    // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6310    // path (core modules resolve to the bare specifier, as in Node).
6311    if name == "__cjs_resolve" {
6312        let spec = with_host(|h| h.str_of(&arg0(&args)));
6313        let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6314        if crate::stdlib::is_core(&spec) {
6315            return Ok(with_host(|h| h.new_str(spec)));
6316        }
6317        return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6318            Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6319            None => Err(crate::host::plain_coded_error(
6320                "Error",
6321                "MODULE_NOT_FOUND",
6322                &format!("Cannot find module '{spec}'"),
6323            )),
6324        };
6325    }
6326    // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6327    // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6328    // stack-introspection pattern used by `depd`), it is called with a synthetic
6329    // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6330    if name == "Error.captureStackTrace" {
6331        let target = arg0(&args);
6332        let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6333        let stack = match prep {
6334            Some(f)
6335                if matches!(
6336                    with_host(|h| h.get(&f).cloned()),
6337                    Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6338                ) =>
6339            {
6340                let sites = crate::module::callsite_stack(10)?;
6341                host::invoke(&f, vec![target.clone(), sites], None)?
6342            }
6343            _ => with_host(|h| h.new_str("")),
6344        };
6345        let _ = set_property(&target, "stack", stack);
6346        return Ok(Value::Undef);
6347    }
6348    // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6349    if let Some(r) = crate::stdlib::call(name, &args) {
6350        return r;
6351    }
6352    match name {
6353        // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6354        // followed by one `    at <site>` line per call site. Reachable because
6355        // the read hands the hook out, and a library may call it directly to
6356        // render a stack it captured.
6357        DEFAULT_PREPARE => {
6358            let err = arg0(&args);
6359            let header = with_host(|h| {
6360                let name = host::lookup_chain(h, &err, "name")
6361                    .map(|v| h.str_of(&v))
6362                    .unwrap_or_else(|| "Error".to_string());
6363                let msg = host::lookup_chain(h, &err, "message")
6364                    .map(|v| h.str_of(&v))
6365                    .unwrap_or_default();
6366                if msg.is_empty() {
6367                    name
6368                } else {
6369                    format!("{name}: {msg}")
6370                }
6371            });
6372            let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6373            let lines = with_host(|h| match h.get(&sites) {
6374                Some(JsObj::Array(items)) => items.clone(),
6375                _ => Vec::new(),
6376            });
6377            let mut out = header;
6378            for s in lines {
6379                let rendered = host::to_string_value(&s)
6380                    .map(|v| with_host(|h| h.str_of(&v)))
6381                    .unwrap_or_default();
6382                out.push_str("\n    at ");
6383                out.push_str(&rendered);
6384            }
6385            Ok(with_host(|h| h.new_str(out)))
6386        }
6387        "console.log" | "console.info" | "console.debug" => {
6388            print_line(&args, false)?;
6389            Ok(Value::Undef)
6390        }
6391        "console.error" | "console.warn" => {
6392            print_line(&args, true)?;
6393            Ok(Value::Undef)
6394        }
6395        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6396        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6397        // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6398        "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6399        "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6400        "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6401        "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6402        "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6403        "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6404        "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6405        "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6406        // Reaching `eval` through this table means the eval FUNCTION VALUE was
6407        // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6408        // Those are INDIRECT evals and run in the global scope. A literal
6409        // `eval(src)` is intercepted earlier, in `host::call_named`.
6410        "eval" => eval_source(args.first(), false),
6411        // `new Function(...)` and `Function(...)` are the same operation
6412        // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6413        // [[Construct]]), so both route to the one generator.
6414        "Function" => function_ctor(&args),
6415        // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6416        // (DEP0005). Node still supports it and still routes it to the same place
6417        // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6418        // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6419        // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6420        // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6421        // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6422        // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6423        // where Node warns.
6424        "Buffer" => {
6425            crate::stdlib::process::emit_deprecation_warning(
6426                "DEP0005",
6427                "Buffer() is deprecated due to security and usability issues. \
6428                 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6429                 Buffer.from() methods instead.",
6430            );
6431            crate::stdlib::construct("Buffer", &args)
6432                .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6433        }
6434        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6435        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6436        "Number.isNaN" => Ok(Value::Bool(
6437            matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6438        )),
6439        "Number.isFinite" => Ok(Value::Bool(
6440            matches!(arg0(&args), Value::Float(f) if f.is_finite())
6441                || matches!(arg0(&args), Value::Int(_)),
6442        )),
6443        "String" => {
6444            if args.is_empty() {
6445                Ok(with_host(|h| h.new_str("")))
6446            } else {
6447                // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6448                // is allowed); everything else via ToString method dispatch.
6449                host::string_ctor_value(&args[0])
6450            }
6451        }
6452        // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6453        // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6454        // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6455        // one — which is why this cannot just call `to_number_value`.
6456        "Number" => Ok(Value::Float(if args.is_empty() {
6457            0.0
6458        } else {
6459            let prim = host::to_primitive(&args[0], "number")?;
6460            match with_host(|h| h.as_bigint(&prim)) {
6461                Some(b) => host::bigint_to_f64(&b),
6462                None => host::to_number_value(&prim)?,
6463            }
6464        })),
6465        "BigInt" => bigint_ctor(&arg0(&args)),
6466        "RegExp" => regexp_ctor(&args),
6467        "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6468        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6469        // Each argument is truncated to a uint16 and taken as one code UNIT, so
6470        // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6471        // a surrogate PAIR of arguments composes into one character.
6472        "String.fromCharCode" => Ok(with_host(|h| {
6473            let units: Vec<u16> = args
6474                .iter()
6475                .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6476                .collect();
6477            let s = crate::utf16::to_string_lossy(&units);
6478            h.new_str(s)
6479        })),
6480        // `fromCodePoint` takes whole code POINTS and rejects anything that is
6481        // not one — including a lone surrogate, which `fromCharCode` accepts.
6482        "String.fromCodePoint" => {
6483            let mut s = String::new();
6484            for a in &args {
6485                let n = with_host(|h| h.to_number(a));
6486                let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6487                {
6488                    char::from_u32(n as u32)
6489                } else {
6490                    None
6491                };
6492                match cp {
6493                    Some(c) => s.push(c),
6494                    None => {
6495                        return Err(format!(
6496                            "RangeError: Invalid code point {}",
6497                            with_host(|h| h.str_of(a))
6498                        ))
6499                    }
6500                }
6501            }
6502            Ok(new_s(s))
6503        }
6504        "String.raw" => string_raw(&args),
6505        // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6506        "Array" => construct_builtin("Array", args),
6507        "Array.of" => construct_array_like(host::current_static_this(), args),
6508        // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6509        // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6510        "Array.isArray" => {
6511            let v = arg0(&args);
6512            let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6513            Ok(Value::Bool(
6514                matches!(
6515                    with_host(|h| h.get(&subject).cloned()),
6516                    Some(JsObj::Array(_))
6517                ) && !is_arguments(&subject),
6518            ))
6519        }
6520        "Array.from" => array_from(args),
6521        "Array.fromAsync" => array_from_async(args),
6522        "Object" => Ok(object_call(args)),
6523        "Object.keys" => object_keys(args, 0),
6524        "Object.values" => object_keys(args, 1),
6525        "Object.entries" => object_keys(args, 2),
6526        "Object.assign" => object_assign(args),
6527        "Object.freeze" => {
6528            let v = arg0(&args);
6529            reject_sealing_a_view(&v, "freeze")?;
6530            if seal_proxy(&v, true)? {
6531                return Ok(v);
6532            }
6533            with_host(|h| h.seal_object(&v, true));
6534            Ok(v)
6535        }
6536        "Object.seal" => {
6537            let v = arg0(&args);
6538            reject_sealing_a_view(&v, "seal")?;
6539            if seal_proxy(&v, false)? {
6540                return Ok(v);
6541            }
6542            with_host(|h| h.seal_object(&v, false));
6543            Ok(v)
6544        }
6545        "Object.preventExtensions" => {
6546            let v = arg0(&args);
6547            if crate::proxy::prevent_extensions(&v)? {
6548                return Ok(v);
6549            }
6550            with_host(|h| h.prevent_extensions(&v));
6551            Ok(v)
6552        }
6553        // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6554        // without coercion: it is not extensible, and vacuously frozen and
6555        // sealed. Reporting it extensible and unfrozen was the opposite of
6556        // every one of the three.
6557        "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6558        "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6559        "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6560        "Object.isFrozen" => integrity_level(&arg0(&args), true),
6561        "Object.isSealed" => integrity_level(&arg0(&args), false),
6562        "Object.isExtensible" => {
6563            let v = arg0(&args);
6564            match crate::proxy::is_extensible(&v)? {
6565                Some(b) => Ok(Value::Bool(b)),
6566                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6567            }
6568        }
6569        // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6570        // distinct from -0.
6571        "Object.is" => {
6572            let a = arg0(&args);
6573            let b = args.get(1).cloned().unwrap_or(Value::Undef);
6574            let num = |v: &Value| match v {
6575                Value::Int(n) => Some(*n as f64),
6576                Value::Float(f) => Some(*f),
6577                _ => None,
6578            };
6579            let r = match (num(&a), num(&b)) {
6580                (Some(x), Some(y)) => {
6581                    if x.is_nan() && y.is_nan() {
6582                        true
6583                    } else if x == 0.0 && y == 0.0 {
6584                        x.is_sign_negative() == y.is_sign_negative()
6585                    } else {
6586                        x == y
6587                    }
6588                }
6589                _ => with_host(|h| h.strict_eq(&a, &b)),
6590            };
6591            Ok(Value::Bool(r))
6592        }
6593        "Object.fromEntries" => object_from_entries(args),
6594        // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6595        // so the proxy form cannot share `prototype_of`'s infallible signature.
6596        // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6597        // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6598        "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6599            if name == "Reflect.getPrototypeOf" {
6600                reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6601            }
6602            let v = arg0(&args);
6603            match crate::proxy::get_prototype_of(&v)? {
6604                Some(p) => Ok(p),
6605                None => Ok(prototype_of(&v)),
6606            }
6607        }
6608        "Object.setPrototypeOf" => {
6609            let obj = arg0(&args);
6610            let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6611            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6612                reject_bad_prototype(&proto)?;
6613                crate::proxy::set_prototype_of(&obj, &proto)?;
6614                return Ok(obj);
6615            }
6616            // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6617            // prototype type check, then — only for an actual object target —
6618            // the extensibility check. A PRIMITIVE target is returned untouched
6619            // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6620            // extensibility test cannot come first.
6621            if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6622                return Err(host::type_error(
6623                    "Object.setPrototypeOf called on null or undefined",
6624                ));
6625            }
6626            reject_bad_prototype(&proto)?;
6627            if with_host(|h| is_object_like(h, &obj)) {
6628                // Setting the SAME prototype is a no-op and stays legal even on a
6629                // frozen object: node v26.7.0 accepts
6630                // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6631                // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6632                // link still has `Object.prototype`, and comparing against the
6633                // absent link would call that a change.
6634                if would_cycle(&obj, &proto) {
6635                    return Err(host::type_error("Cyclic __proto__ value"));
6636                }
6637                if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6638                    // The receiver is named by its brand, as every other
6639                    // refusal names it — a NULL-PROTOTYPE object is
6640                    // `[object Object]`, not `#<Object>`, because it has no
6641                    // constructor to name.
6642                    return Err(host::type_error(&format!(
6643                        "{} is not extensible",
6644                        no_side_effects_string(&obj)
6645                    )));
6646                }
6647                with_host(|h| h.set_proto(&obj, proto));
6648            }
6649            Ok(obj)
6650        }
6651        "Object.create" => object_create(args),
6652        "Object.getOwnPropertyNames" => object_keys(args, 3),
6653        "Object.getOwnPropertySymbols" => {
6654            let v = arg0(&args);
6655            require_object_coercible(&v)?;
6656            let syms = proxy_or_own_symbol_keys(&v)?;
6657            Ok(with_host(|h| h.new_array(syms)))
6658        }
6659        // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6660        "Object.hasOwn" => {
6661            let obj = arg0(&args);
6662            let key = args.get(1).cloned().unwrap_or(Value::Undef);
6663            object_builtin_method(&obj, "hasOwnProperty", vec![key])
6664        }
6665        "Object.defineProperty" => object_define_property(args),
6666        "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6667        "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6668        "Object.defineProperties" => object_define_properties(args),
6669        // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6670        // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6671        "Object.groupBy" => object_group_by(args),
6672        "Symbol" => Ok(with_host(|h| {
6673            let desc = args
6674                .first()
6675                .filter(|a| !matches!(a, Value::Undef))
6676                .map(|a| h.str_of(a));
6677            h.new_symbol(desc)
6678        })),
6679        "Symbol.for" => Ok(with_host(|h| {
6680            let key = h.str_of(&arg0(&args));
6681            h.symbol_for(&key)
6682        })),
6683        // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6684        // description read: it answers only for symbols `Symbol.for` created.
6685        // Returning the description made every symbol look registered —
6686        // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6687        "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6688        "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6689        // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6690        "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6691        "Proxy.revocable" => crate::proxy::revocable(&args),
6692        // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6693        // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6694        // own properties, so there is no second half to append).
6695        // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6696        // non-enumerable included, strings first and then the SYMBOLS.
6697        "Reflect.ownKeys" => {
6698            let v = arg0(&args);
6699            reflect_require_object(&v, "ownKeys")?;
6700            let names = object_keys(args, 3)?;
6701            let syms = proxy_or_own_symbol_keys(&v)?;
6702            if syms.is_empty() {
6703                return Ok(names);
6704            }
6705            let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6706            all.extend(syms);
6707            Ok(with_host(|h| h.new_array(all)))
6708        }
6709        "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6710        // `Reflect.defineProperty` REPORTS success as a boolean where
6711        // `Object.defineProperty` throws (28.1.3). It was propagating the
6712        // throw, so the whole point of the reflective form was lost.
6713        "Reflect.defineProperty" => {
6714            reflect_require_object(&arg0(&args), "defineProperty")?;
6715            Ok(Value::Bool(object_define_property(args).is_ok()))
6716        }
6717        "Reflect.deleteProperty" => {
6718            let obj = arg0(&args);
6719            reflect_require_object(&obj, "deleteProperty")?;
6720            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6721            Ok(Value::Bool(delete_property(&obj, &k)?))
6722        }
6723        "Reflect.setPrototypeOf" => {
6724            let obj = arg0(&args);
6725            let p = args.get(1).cloned().unwrap_or(Value::Undef);
6726            if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6727                crate::proxy::set_prototype_of(&obj, &p)?;
6728                return Ok(Value::Bool(true));
6729            }
6730            // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6731            // unless the new one is what it already has, which is a no-op. It
6732            // reported success and rewrote the link.
6733            // `Reflect` reports a refusal rather than throwing, for a cycle as
6734            // for a non-extensible receiver.
6735            if would_cycle(&obj, &p) {
6736                return Ok(Value::Bool(false));
6737            }
6738            if !with_host(|h| h.is_extensible(&obj)) {
6739                return Ok(Value::Bool(same_prototype(&obj, &p)));
6740            }
6741            with_host(|h| h.set_proto(&obj, p));
6742            Ok(Value::Bool(true))
6743        }
6744        "Reflect.isExtensible" => {
6745            let v = arg0(&args);
6746            match crate::proxy::is_extensible(&v)? {
6747                Some(b) => Ok(Value::Bool(b)),
6748                None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6749            }
6750        }
6751        "Reflect.preventExtensions" => {
6752            let v = arg0(&args);
6753            if crate::proxy::prevent_extensions(&v)? {
6754                return Ok(Value::Bool(true));
6755            }
6756            with_host(|h| h.prevent_extensions(&v));
6757            Ok(Value::Bool(true))
6758        }
6759        // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6760        "Reflect.apply" => {
6761            let f = arg0(&args);
6762            let this = args.get(1).cloned();
6763            let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6764            host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6765        }
6766        // `Reflect.construct(target, args, newTarget)` — the optional third
6767        // argument decides which constructor's `prototype` the instance gets
6768        // (28.1.2). It was ignored, so the result always inherited from
6769        // `target` and `instanceof newTarget` was false.
6770        "Reflect.construct" => {
6771            let f = arg0(&args);
6772            let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6773            let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6774            host::construct_nt(&f, list, new_target)
6775        }
6776        "Reflect.has" => {
6777            let obj = arg0(&args);
6778            reflect_require_object(&obj, "has")?;
6779            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6780            Ok(Value::Bool(has_property(&obj, &k)?))
6781        }
6782        // `Reflect.get(target, key, receiver)` — the optional third argument is
6783        // what a getter sees as `this` (28.1.6). Defaults to the target.
6784        "Reflect.get" => {
6785            let obj = arg0(&args);
6786            reflect_require_object(&obj, "get")?;
6787            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6788            let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6789            get_property_recv(&obj, &k, &receiver)
6790        }
6791        // `Reflect.set(target, key, value, receiver)` — the optional fourth
6792        // argument is what a setter sees as `this`, and where a DATA property
6793        // lands (28.1.13). It was ignored: the setter ran against the target
6794        // and the property was written there.
6795        "Reflect.set" => {
6796            let obj = arg0(&args);
6797            reflect_require_object(&obj, "set")?;
6798            let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6799            let v = args.get(2).cloned().unwrap_or(Value::Undef);
6800            let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6801            Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6802        }
6803        "JSON.stringify" => json_stringify(args),
6804        "JSON.parse" => json_parse(args),
6805        "JSON.rawJSON" => json_raw(args),
6806        "JSON.isRawJSON" => json_is_raw(args),
6807        "structuredClone" => structured_clone(args),
6808        // The deferred drain a `Readable.from` schedules; the suffix is the
6809        // stream's heap index.
6810        _ if name.starts_with("@@transformCb:") => {
6811            let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6812            crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6813            Ok(Value::Undef)
6814        }
6815        _ if name.starts_with("@@streamFlush:") => {
6816            let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6817            crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6818            Ok(Value::Undef)
6819        }
6820        // Same implementation the `buffer` module exposes; only the binding was
6821        // missing.
6822        "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6823            .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6824        "fetch" => crate::stdlib::fetch::fetch(&args),
6825        // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6826        // suffix is the signal's heap index.
6827        _ if name.starts_with("@@aborttimeout:") => {
6828            let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6829            crate::stdlib::fetch::fire_timeout_abort(idx)
6830        }
6831        // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6832        // implementation. Nothing here waits on backpressure, so it only has to
6833        // BE callable — an implementation that ends with `cb()`, which the
6834        // stream contract requires, would otherwise throw.
6835        "@@streamWriteCallback" => Ok(Value::Undef),
6836        "queueMicrotask" | "process.nextTick" => {
6837            let cb = arg0(&args);
6838            require_callback(&cb)?;
6839            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6840            enqueue_microtask(name == "process.nextTick", cb, rest);
6841            Ok(Value::Undef)
6842        }
6843        "setTimeout" | "setInterval" | "setImmediate" => {
6844            require_callback(&arg0(&args))?;
6845            Ok(schedule_timer(name, args))
6846        }
6847        "clearTimeout" | "clearInterval" | "clearImmediate" => {
6848            clear_timer(&arg0(&args));
6849            Ok(Value::Undef)
6850        }
6851        "Promise.resolve" => promise_resolve(arg0(&args)),
6852        "Promise.reject" => promise_reject(arg0(&args)),
6853        "Promise.all" => promise_all(args, AllMode::All),
6854        "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6855        "Promise.race" => promise_race(args, false),
6856        "Promise.any" => promise_race(args, true),
6857        // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6858        // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6859        "Promise.withResolvers" => promise_with_resolvers(),
6860        "Promise.try" => promise_try(args),
6861        "RegExp.escape" => regexp_escape(args),
6862        "Error.isError" => error_is_error(args),
6863        // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6864        // `cb(item, i)` result (SameValueZero), each value an array of members.
6865        "Map.groupBy" => map_group_by(args),
6866        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6867        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6868        // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6869        // The executor a species-constructed promise is built with: it does
6870        // nothing, because the caller settles the result through its id.
6871        "@@pnoop" => Ok(Value::Undef),
6872        _ if name.starts_with("@@presolve:") => {
6873            let id: u32 = name[11..].parse().unwrap_or(0);
6874            host::resolve_promise_val(id, arg0(&args));
6875            Ok(Value::Undef)
6876        }
6877        _ if name.starts_with("@@preject:") => {
6878            let id: u32 = name[10..].parse().unwrap_or(0);
6879            host::reject_promise_val(id, arg0(&args));
6880            Ok(Value::Undef)
6881        }
6882        // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6883        // index so calling it twice is the spec's no-op rather than a re-tear.
6884        _ if name.starts_with("@@prevoke:") => {
6885            let i: u32 = name[10..].parse().unwrap_or(0);
6886            Ok(crate::proxy::revoke(i))
6887        }
6888        _ if name.starts_with("@@finpass:") => {
6889            // finally(cb) on fulfill: run cb, await whatever it returned, then
6890            // pass the original value through.
6891            let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6892            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6893            Ok(finally_chain(result, arg0(&args), false))
6894        }
6895        _ if name.starts_with("@@finthrow:") => {
6896            // finally(cb) on reject: same, then re-throw the original reason.
6897            let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6898            let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6899            Ok(finally_chain(result, arg0(&args), true))
6900        }
6901        // The two thunks `finally_chain` hangs off that awaited promise. Each
6902        // carries the value it must reinstate in a one-slot cell, since a
6903        // builtin is identified only by its name and cannot close over one.
6904        _ if name.starts_with("@@finret:") => {
6905            let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6906            get_property(&Value::Obj(i), "0")
6907        }
6908        _ if name.starts_with("@@finrethrow:") => {
6909            let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6910            let reason = get_property(&Value::Obj(i), "0")?;
6911            with_host(|h| h.exc = Some(reason.clone()));
6912            Err(with_host(|h| error_string(h, &reason)))
6913        }
6914        _ => Err(host::type_error(&format!("{name} is not a function"))),
6915    }
6916}
6917
6918/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6919/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6920/// (matching Node's messages).
6921/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6922/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6923/// BigInt`. The old text said "value" literally, for every input.
6924fn bigint_convert_error(v: &Value) -> String {
6925    let shown = with_host(|h| h.str_of(v));
6926    host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6927}
6928
6929/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6930/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6931/// BigInt arithmetic's operand check.
6932///
6933/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6934/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6935/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6936/// the type table — booleans convert (`true` → `1n`), strings parse with a
6937/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6938///
6939/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6940///
6941/// ```text
6942/// a[0] = true            → 1n
6943/// a[0] = '12'            → 12n
6944/// a[0] = []              → 0n        (ToPrimitive → "" → 0n)
6945/// a[0] = ['3']           → 3n
6946/// a[0] = 1               → TypeError: Cannot convert 1 to a BigInt
6947/// a[0] = new Number(3)   → TypeError: Cannot convert 3 to a BigInt
6948/// a[0] = 'a'             → SyntaxError: Cannot convert a to a BigInt
6949/// a[0] = {}              → SyntaxError: Cannot convert [object Object] to a BigInt
6950/// ```
6951pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6952    let prim = host::to_primitive(v, "number")?;
6953    if let Some(b) = with_host(|h| match h.get(&prim) {
6954        Some(JsObj::BigInt(b)) => Some(b.clone()),
6955        _ => None,
6956    }) {
6957        return Ok(b);
6958    }
6959    match &prim {
6960        Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6961        Value::Str(s) => host::parse_bigint_str(s)
6962            .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6963        _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6964            let s = with_host(|h| h.str_of(&prim));
6965            host::parse_bigint_str(&s)
6966                .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6967        }
6968        _ => Err(bigint_convert_error(&prim)),
6969    }
6970}
6971
6972fn bigint_ctor(v: &Value) -> Result<Value, String> {
6973    use num_bigint::BigInt;
6974    let big = match v {
6975        Value::Bool(b) => BigInt::from(*b as i64),
6976        Value::Int(n) => BigInt::from(*n),
6977        Value::Float(f) => {
6978            if !f.is_finite() || f.fract() != 0.0 {
6979                let disp = with_host(|h| h.str_of(v));
6980                return Err(format!(
6981                    "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6982                ));
6983            }
6984            // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6985            // .toString` switches to exponential notation at 1e21, and
6986            // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6987            // threw `Cannot convert value to a BigInt` where node returns
6988            // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6989            // value, which is also what node reports for a magnitude past the
6990            // exactly-representable range (`BigInt(1e30)` is
6991            // `1000000000000000019884624838656n` in both).
6992            match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6993                Some(b) => b,
6994                None => return Err(bigint_convert_error(v)),
6995            }
6996        }
6997        Value::Str(s) => match host::parse_bigint_str(s) {
6998            Some(b) => b,
6999            None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
7000        },
7001        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7002            Some(JsObj::BigInt(b)) => b,
7003            Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
7004                Some(b) => b,
7005                None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
7006            },
7007            _ => return Err(bigint_convert_error(v)),
7008        },
7009        _ => return Err(bigint_convert_error(v)),
7010    };
7011    Ok(with_host(|h| h.new_bigint(big)))
7012}
7013
7014/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
7015/// its source (and flags, unless new ones are given).
7016fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
7017    let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
7018        Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
7019        _ => {
7020            let a0 = arg0(args);
7021            // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
7022            // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
7023            // pattern instead of throwing.
7024            let src = if matches!(a0, Value::Undef) {
7025                String::new()
7026            } else {
7027                arg_to_string(args, 0)?
7028            };
7029            (src, None)
7030        }
7031    };
7032    let flags = match args.get(1) {
7033        Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
7034        _ => existing_flags.unwrap_or_default(),
7035    };
7036    // An empty source compiles as the JS canonical `(?:)`.
7037    let src = if source.is_empty() {
7038        "(?:)".to_string()
7039    } else {
7040        source
7041    };
7042    crate::regexp::build_regexp(&src, &flags)
7043}
7044
7045/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
7046/// two's-complement (signed) or unsigned integer.
7047fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
7048    use num_bigint::BigInt;
7049    use num_traits::Signed;
7050    let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
7051    if bits < 0 {
7052        return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7053    }
7054    let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7055        Some(b) => b,
7056        None => return Err(host::type_error("Cannot convert to a BigInt")),
7057    };
7058    let bits = bits as u32;
7059    if bits == 0 {
7060        return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7061    }
7062    let modulus = BigInt::from(1) << bits; // 2^bits
7063                                           // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7064    let mut r = &x % &modulus;
7065    if r.is_negative() {
7066        r += &modulus;
7067    }
7068    if !unsigned {
7069        let half = BigInt::from(1) << (bits - 1);
7070        if r >= half {
7071            r -= &modulus;
7072        }
7073    }
7074    Ok(with_host(|h| h.new_bigint(r)))
7075}
7076
7077/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7078/// interleaved with the substitutions.
7079fn string_raw(args: &[Value]) -> Result<Value, String> {
7080    let call_site = arg0(args);
7081    let raw = get_property(&call_site, "raw")?;
7082    let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7083    let mut out = String::new();
7084    for (i, r) in raws.iter().enumerate() {
7085        out.push_str(&with_host(|h| h.str_of(r)));
7086        if i + 1 < raws.len() {
7087            if let Some(sub) = args.get(i + 1) {
7088                out.push_str(&with_host(|h| h.str_of(sub)));
7089            }
7090        }
7091    }
7092    Ok(with_host(|h| h.new_str(out)))
7093}
7094
7095/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7096/// fresh object; objects pass through.
7097/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7098/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7099/// ordinary object that also has internal slots, so it can carry own properties
7100/// like anything else — but its heap variant holds only those slots, so a write
7101/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7102pub fn uses_side_table(v: &Value) -> bool {
7103    matches!(
7104        with_host(|h| h.kind_of(v)),
7105        Some(
7106            ObjKind::Map
7107                | ObjKind::Set
7108                | ObjKind::Promise
7109                | ObjKind::RegExp
7110                | ObjKind::Generator
7111                | ObjKind::Symbol
7112                | ObjKind::BigInt
7113                | ObjKind::Iter
7114        )
7115    )
7116}
7117
7118fn object_call(args: Vec<Value>) -> Value {
7119    let a = arg0(&args);
7120    // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7121    // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7122    if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7123        return with_host(|h| h.new_object(IndexMap::new()));
7124    }
7125    to_object(&a)
7126}
7127
7128/// The name of the wrapper a primitive boxes into, or `None` when the value is
7129/// already an object.
7130fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7131    match v {
7132        Value::Int(_) | Value::Float(_) => Some("Number"),
7133        Value::Bool(_) => Some("Boolean"),
7134        Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7135            Some(JsObj::Str(_)) => Some("String"),
7136            Some(JsObj::Symbol { .. }) => Some("Symbol"),
7137            Some(JsObj::BigInt(_)) => Some("BigInt"),
7138            _ => None,
7139        },
7140        _ => None,
7141    }
7142}
7143
7144/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7145/// for every other value. The slot is a hidden `@@primitive` own property —
7146/// the same `@@` marker convention the engine already uses for internal state,
7147/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7148pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7149    with_host(|h| match h.get(v) {
7150        Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7151        _ => None,
7152    })
7153}
7154
7155/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7156/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7157///
7158/// A String wrapper also owns its index properties and `length`, which is what
7159/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7160/// non-writable and non-configurable, as the exotic `String` object's are.
7161pub fn to_object(v: &Value) -> Value {
7162    let Some(ctor) = wrapper_ctor_of(v) else {
7163        return v.clone();
7164    };
7165    with_host(|h| h.ensure_wrapper_protos());
7166    let chars: Vec<String> = if ctor == "String" {
7167        with_host(|h| h.str_of(v))
7168            .chars()
7169            .map(|c| c.to_string())
7170            .collect()
7171    } else {
7172        Vec::new()
7173    };
7174    with_host(|h| {
7175        let mut m: IndexMap<String, Value> = IndexMap::new();
7176        for (i, c) in chars.iter().enumerate() {
7177            let s = h.new_str(c.clone());
7178            m.insert(i.to_string(), s);
7179        }
7180        let w = h.new_object(m);
7181        if ctor == "String" {
7182            for i in 0..chars.len() {
7183                h.set_prop_attrs(
7184                    &w,
7185                    &i.to_string(),
7186                    host::PropAttrs {
7187                        writable: false,
7188                        enumerable: true,
7189                        configurable: false,
7190                    },
7191                );
7192            }
7193            let len = Value::Float(chars.len() as f64);
7194            if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7195                p.insert("length".into(), len);
7196            }
7197            h.set_prop_attrs(
7198                &w,
7199                "length",
7200                host::PropAttrs {
7201                    writable: false,
7202                    enumerable: false,
7203                    configurable: false,
7204                },
7205            );
7206        }
7207        if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7208            p.insert("@@primitive".into(), v.clone());
7209        }
7210        if let Some(proto) = h.native_proto(ctor) {
7211            h.set_proto(&w, proto);
7212        }
7213        w
7214    })
7215}
7216
7217/// Construct via `new` for the builtin constructors.
7218pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7219    // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7220    if let Some(r) = crate::stdlib::construct(name, &args) {
7221        return r;
7222    }
7223    match name {
7224        "Array" => {
7225            // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7226            // A single NUMBER argument is a length and is validated as one
7227            // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7228            // `new Array(2**32)` are all `RangeError: Invalid array length` on
7229            // node v26.7.0; only a non-number single argument is an element.
7230            if args.len() == 1 {
7231                if let Value::Float(_) | Value::Int(_) = args[0] {
7232                    let n = host::to_array_length(&args[0])?;
7233                    // Every element of `new Array(n)` is a HOLE, not a stored
7234                    // `undefined`: `Object.keys(Array(3))` is `[]`.
7235                    return Ok(with_host(|h| {
7236                        let a = h.new_array(vec![Value::Undef; n]);
7237                        h.mark_hole_range(&a, 0..n);
7238                        a
7239                    }));
7240                }
7241            }
7242            Ok(with_host(|h| h.new_array(args)))
7243        }
7244        "Object" => Ok(object_call(args)),
7245        // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7246        // form. These were not constructors at all, so every one threw.
7247        "String" => Ok(to_object(&host::to_string_value(
7248            &args
7249                .first()
7250                .cloned()
7251                .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7252        )?)),
7253        "Number" => Ok(to_object(&Value::Float(match args.first() {
7254            Some(a) => host::to_number_value(a)?,
7255            None => 0.0,
7256        }))),
7257        "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7258            h.truthy(&arg0(&args))
7259        })))),
7260        "Map" | "WeakMap" => {
7261            let weak = name == "WeakMap";
7262            let m = with_host(|h| {
7263                h.alloc(JsObj::Map {
7264                    entries: indexmap::IndexMap::new(),
7265                    weak,
7266                })
7267            });
7268            if let Some(init) = args
7269                .first()
7270                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7271            {
7272                // Stepped, not drained: an entry that is not a pair has to
7273                // stop the construction at that element and CLOSE the iterator
7274                // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7275                // infinite source was never reached and the constructor HUNG.
7276                host::iter_for_each(init, |p, _| {
7277                    // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7278                    // is iterable, so without this check `new Map(["ab"])`
7279                    // happily stored `'a' => 'b'` instead of throwing — and over
7280                    // an infinite source it never stopped.
7281                    if !with_host(|h| is_object_like(h, &p)) {
7282                        let shown = with_host(|h| h.str_of(&p));
7283                        return Err(host::type_error(&format!(
7284                            "Iterator value {shown} is not an entry object"
7285                        )));
7286                    }
7287                    // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7288                    // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7289                    // gives `undefined => undefined`, and an array-LIKE entry
7290                    // works. Iterating it instead accepted a string as a pair
7291                    // and rejected the array-like.
7292                    let k = get_property(&p, "0")?;
7293                    let v = get_property(&p, "1")?;
7294                    map_method(&m, "set", vec![k, v])?;
7295                    Ok(())
7296                })?;
7297            }
7298            Ok(m)
7299        }
7300        "Set" | "WeakSet" => {
7301            let weak = name == "WeakSet";
7302            let s = with_host(|h| {
7303                h.alloc(JsObj::Set {
7304                    entries: indexmap::IndexMap::new(),
7305                    weak,
7306                })
7307            });
7308            if let Some(init) = args
7309                .first()
7310                .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7311            {
7312                host::iter_for_each(init, |v, _| {
7313                    set_method(&s, "add", vec![v])?;
7314                    Ok(())
7315                })?;
7316            }
7317            Ok(s)
7318        }
7319        "Promise" => new_promise(arg0(&args)),
7320        "Proxy" => crate::proxy::create(&args),
7321        // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7322        // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7323        // deprecation wrapper this way, so `require('body-parser')` — and with it
7324        // `require('express')` — dies at load without it.
7325        "Function" => function_ctor(&args),
7326        "RegExp" => regexp_ctor(&args),
7327        "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7328        "Error" => make_error_checked(name, &args),
7329        // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7330        // legacy numeric `code` follows from it.
7331        "DOMException" => Ok(dom_exception(&args)),
7332        n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7333        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7334    }
7335}
7336
7337/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7338/// outside the table — including the default `"Error"` — reports 0.
7339pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7340    ("IndexSizeError", 1.0),
7341    ("DOMStringSizeError", 2.0),
7342    ("HierarchyRequestError", 3.0),
7343    ("WrongDocumentError", 4.0),
7344    ("InvalidCharacterError", 5.0),
7345    ("NoDataAllowedError", 6.0),
7346    ("NoModificationAllowedError", 7.0),
7347    ("NotFoundError", 8.0),
7348    ("NotSupportedError", 9.0),
7349    ("InUseAttributeError", 10.0),
7350    ("InvalidStateError", 11.0),
7351    ("SyntaxError", 12.0),
7352    ("InvalidModificationError", 13.0),
7353    ("NamespaceError", 14.0),
7354    ("InvalidAccessError", 15.0),
7355    ("ValidationError", 16.0),
7356    ("TypeMismatchError", 17.0),
7357    ("SecurityError", 18.0),
7358    ("NetworkError", 19.0),
7359    ("AbortError", 20.0),
7360    ("URLMismatchError", 21.0),
7361    ("QuotaExceededError", 22.0),
7362    ("TimeoutError", 23.0),
7363    ("InvalidNodeTypeError", 24.0),
7364    ("DataCloneError", 25.0),
7365];
7366
7367/// The static name a `DOMException` code is exposed under: the error name minus
7368/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7369/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7370fn legacy_code_name(error_name: &str) -> String {
7371    let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7372    let mut out = String::new();
7373    for (i, c) in stem.chars().enumerate() {
7374        if c.is_ascii_uppercase() && i > 0 {
7375            out.push('_');
7376        }
7377        out.push(c.to_ascii_uppercase());
7378    }
7379    out.push_str("_ERR");
7380    out
7381}
7382
7383/// `new DOMException(message, name)`.
7384///
7385/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7386/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7387/// legacy number that name maps to.
7388pub fn dom_exception(args: &[Value]) -> Value {
7389    let message = match args.first() {
7390        None | Some(Value::Undef) => String::new(),
7391        Some(v) => with_host(|h| h.str_of(v)),
7392    };
7393    let name = match args.get(1) {
7394        None | Some(Value::Undef) => "Error".to_string(),
7395        Some(v) => with_host(|h| h.str_of(v)),
7396    };
7397    with_host(|h| dom_exception_with(h, &name, &message))
7398}
7399
7400/// `dom_exception` for a caller that already holds the host borrow.
7401pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7402    let name = name.to_string();
7403    let message = message.to_string();
7404    let code = DOM_EXCEPTION_CODES
7405        .iter()
7406        .find(|(n, _)| *n == name)
7407        .map(|(_, c)| *c)
7408        .unwrap_or(0.0);
7409    let head = if message.is_empty() {
7410        name.clone()
7411    } else {
7412        format!("{name}: {message}")
7413    };
7414    let e = synth_error(h, &head);
7415    {
7416        let nv = h.new_str(name);
7417        let mv = h.new_str(message);
7418        let sv = h.new_str(head);
7419        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7420            // `name`, `message` and `code` are PROTOTYPE accessors over internal
7421            // slots in node, so `stack` is the instance's only own property.
7422            // Storing them as own keys would show up in
7423            // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7424            p.shift_remove("message");
7425            p.insert("@@domName".into(), nv);
7426            p.insert("@@domMessage".into(), mv);
7427            p.insert("@@domCode".into(), Value::Float(code));
7428            p.insert("stack".into(), sv);
7429        }
7430        h.ensure_error_protos();
7431        if let Some(proto) = host::error_proto_of(h, "DOMException") {
7432            h.set_proto(&e, proto);
7433        }
7434    }
7435    e
7436}
7437
7438/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7439/// rather than as own properties. `None` for anything else.
7440pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7441    let slot = match name {
7442        "name" => "@@domName",
7443        "message" => "@@domMessage",
7444        "code" => "@@domCode",
7445        _ => return None,
7446    };
7447    with_host(|h| match h.get(recv) {
7448        Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7449        _ => None,
7450    })
7451}
7452
7453/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7454/// throw a value with extra own properties on it.
7455pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7456    let m = with_host(|h| h.new_str(msg.to_string()));
7457    make_error_inner(name, &[m])
7458}
7459
7460/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7461/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7462/// rendered `Symbol(desc)` into `.message`.
7463fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7464    if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7465        // AggregateError's message is its SECOND argument.
7466        let idx = usize::from(name == "AggregateError");
7467        if idx == 0 {
7468            host::to_string_value(m)?;
7469        } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7470            host::to_string_value(m2)?;
7471        }
7472    }
7473    Ok(make_error_inner(name, args))
7474}
7475
7476fn make_error_inner(name: &str, args: &[Value]) -> Value {
7477    // `new AggregateError(errors, message)` takes the causes FIRST; every other
7478    // error constructor takes the message first.
7479    let agg = name == "AggregateError";
7480    let (errors, args) = if agg {
7481        (
7482            Some(args.first().cloned().unwrap_or(Value::Undef)),
7483            args.get(1..).unwrap_or(&[]),
7484        )
7485    } else {
7486        (None, args)
7487    };
7488    with_host(|h| {
7489        h.ensure_error_protos();
7490        let mut props: IndexMap<String, Value> = IndexMap::new();
7491        let msg = args
7492            .first()
7493            .filter(|a| !matches!(a, Value::Undef))
7494            .map(|a| h.str_of(a));
7495        if let Some(m) = &msg {
7496            let mv = h.new_str(m.clone());
7497            props.insert("message".into(), mv);
7498        }
7499        // `.stack` is engine-specific; a simple `Name: message` header line
7500        // suffices for parity (the fuzzer never prints raw stacks).
7501        //
7502        // V8 formats that header LAZILY, on the first read, from whatever `name`
7503        // and `message` the error carries at that moment — which is why the
7504        // near-universal
7505        //
7506        //     class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7507        //
7508        // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7509        // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7510        // carries the frames so the read can redo it; see `materialize_stack`.
7511        let frames = h.stack_frames();
7512        let stack = match &msg {
7513            Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7514            _ => format!("{name}{frames}"),
7515        };
7516        let sv = h.new_str(stack);
7517        props.insert("stack".into(), sv);
7518        let raw = h.new_str(frames);
7519        props.insert("@@stackRaw".into(), raw);
7520        if let Some(errs) = errors {
7521            // Materialize the iterable into the own `errors` array property.
7522            let items = h.iter_vec(&errs).unwrap_or_default();
7523            let arr = h.new_array(items);
7524            props.insert("errors".into(), arr);
7525        }
7526        // `new Error(msg, { cause })` (ES2022): installed only when the options
7527        // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7528        let opts = args.get(1);
7529        if let Some(cause) = opts.and_then(|o| match h.get(o) {
7530            Some(JsObj::Object(p)) => p.get("cause").cloned(),
7531            _ => None,
7532        }) {
7533            props.insert("cause".into(), cause);
7534        }
7535        let e = h.new_object(props);
7536        if let Some(p) = host::error_proto_of(h, name) {
7537            h.set_proto(&e, p);
7538        }
7539        // Every own slot an error constructor installs is non-enumerable in V8,
7540        // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7541        // `{}` — properties a *script* later assigns stay enumerable.
7542        for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7543            h.hide_prop(&e, k);
7544        }
7545        e
7546    })
7547}
7548
7549fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7550    // Node's console.log(...args) === util.format(...args): printf-style
7551    // substitution when the first arg is a format string, else inspect-and-join.
7552    // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7553    // the caller instead of printing a line — so nothing is written on failure.
7554    let line: String = crate::stdlib::util::format(args)?;
7555    with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7556    Ok(())
7557}
7558
7559fn arg0(args: &[Value]) -> Value {
7560    args.first().cloned().unwrap_or(Value::Undef)
7561}
7562/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7563/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7564/// instead rendered `Symbol(desc)` into its result and reported nothing.
7565fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7566    let v = args.get(i).cloned().unwrap_or(Value::Undef);
7567    let sv = host::to_string_value(&v)?;
7568    Ok(with_host(|h| h.str_of(&sv)))
7569}
7570
7571fn arg_num(args: &[Value], i: usize) -> f64 {
7572    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7573}
7574
7575fn is_integer(v: Value) -> bool {
7576    match v {
7577        Value::Int(_) => true,
7578        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7579        _ => false,
7580    }
7581}
7582fn is_safe_integer(v: Value) -> bool {
7583    match v {
7584        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7585        Value::Int(_) => true,
7586        _ => false,
7587    }
7588}
7589
7590/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7591/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7592/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7593fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7594    // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7595    const UNRESERVED: &[u8] =
7596        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7597    // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7598    const RESERVED: &[u8] = b";,/?:@&=+$#";
7599    let mut out = String::with_capacity(s.len());
7600    for &b in s.as_bytes() {
7601        if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7602            out.push(b as char);
7603        } else {
7604            out.push('%');
7605            out.push(
7606                char::from_digit((b >> 4) as u32, 16)
7607                    .unwrap()
7608                    .to_ascii_uppercase(),
7609            );
7610            out.push(
7611                char::from_digit((b & 0xf) as u32, 16)
7612                    .unwrap()
7613                    .to_ascii_uppercase(),
7614            );
7615        }
7616    }
7617    Ok(with_host(|h| h.new_str(out)))
7618}
7619
7620/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7621/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7622/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7623fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7624    const RESERVED: &[u8] = b";,/?:@&=+$#";
7625    let bytes = s.as_bytes();
7626    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7627    let mut i = 0;
7628    while i < bytes.len() {
7629        if bytes[i] == b'%' {
7630            if i + 2 >= bytes.len() {
7631                return Err("URIError: URI malformed".into());
7632            }
7633            let hi = (bytes[i + 1] as char).to_digit(16);
7634            let lo = (bytes[i + 2] as char).to_digit(16);
7635            match (hi, lo) {
7636                (Some(h), Some(l)) => {
7637                    let byte = (h * 16 + l) as u8;
7638                    // decodeURI keeps reserved-delimiter escapes literal.
7639                    if uri && RESERVED.contains(&byte) {
7640                        out.extend_from_slice(&bytes[i..i + 3]);
7641                    } else {
7642                        out.push(byte);
7643                    }
7644                    i += 3;
7645                }
7646                _ => return Err("URIError: URI malformed".into()),
7647            }
7648        } else {
7649            out.push(bytes[i]);
7650            i += 1;
7651        }
7652    }
7653    match String::from_utf8(out) {
7654        Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7655        Err(_) => Err("URIError: URI malformed".into()),
7656    }
7657}
7658
7659/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7660/// present in every engine and still reached by old libraries (jQuery's cookie
7661/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7662/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7663/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7664/// character yields the two escapes of its surrogate pair
7665/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7666///
7667/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7668/// it keeps `@*_+-./` and drops `!~'()`.
7669fn legacy_escape(s: &str) -> Result<Value, String> {
7670    const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7671    let mut out = String::with_capacity(s.len());
7672    for u in s.encode_utf16() {
7673        if u < 0x100 {
7674            if KEEP.contains(&(u as u8)) {
7675                out.push(u as u8 as char);
7676            } else {
7677                out.push_str(&format!("%{u:02X}"));
7678            }
7679        } else {
7680            out.push_str(&format!("%u{u:04X}"));
7681        }
7682    }
7683    Ok(with_host(|h| h.new_str(out)))
7684}
7685
7686/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7687/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7688/// `%XX` or `%uXXXX` escape is passed through literally
7689/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7690///
7691/// Decoding is done in code-unit space and re-joined at the end so a
7692/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7693fn legacy_unescape(s: &str) -> Result<Value, String> {
7694    let b = s.as_bytes();
7695    let hex = |i: usize, n: usize| -> Option<u16> {
7696        if i + n > b.len() {
7697            return None;
7698        }
7699        let mut v: u16 = 0;
7700        for &c in &b[i..i + n] {
7701            v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7702        }
7703        Some(v)
7704    };
7705    let units: Vec<u16> = s.encode_utf16().collect();
7706    let mut out: Vec<u16> = Vec::with_capacity(units.len());
7707    let mut i = 0;
7708    while i < b.len() {
7709        // Escapes are pure ASCII, so a byte index is a unit index up to here —
7710        // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7711        if b[i] == b'%' {
7712            if let Some(u) = hex(i + 1, 2) {
7713                out.push(u);
7714                i += 3;
7715                continue;
7716            }
7717            if b.get(i + 1) == Some(&b'u') {
7718                if let Some(u) = hex(i + 2, 4) {
7719                    out.push(u);
7720                    i += 6;
7721                    continue;
7722                }
7723            }
7724        }
7725        let c = s[i..].chars().next().unwrap_or('%');
7726        let mut buf = [0u16; 2];
7727        out.extend_from_slice(c.encode_utf16(&mut buf));
7728        i += c.len_utf8();
7729    }
7730    Ok(with_host(|h| {
7731        h.new_str(crate::utf16::to_string_lossy(&out))
7732    }))
7733}
7734
7735/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7736/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7737/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7738/// instead of converting it, which both swallowed that throw and ignored any
7739/// `toString` the value defines.
7740fn parse_int(args: &[Value]) -> Result<f64, String> {
7741    // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7742    let sv = host::to_string_value(&arg0(args))?;
7743    // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7744    // infallible read below does no `ToPrimitive`, so an object radix came out
7745    // as NaN and the parse silently fell back to auto-detection.
7746    let radix = match args.get(1) {
7747        Some(r) if !matches!(r, Value::Undef) => {
7748            vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7749        }
7750        _ => args.to_vec(),
7751    };
7752    Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7753}
7754
7755fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7756    // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7757    // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7758    // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7759    let radix_arg = args
7760        .get(1)
7761        .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7762    let radix = match radix_arg {
7763        Some(0) | None => None,
7764        Some(r) if (2..=36).contains(&r) => Some(r as u32),
7765        Some(_) => return f64::NAN,
7766    };
7767    let t = crate::utf16::js_trim_start(s);
7768    let (neg, digits) = match t.strip_prefix('-') {
7769        Some(rest) => (true, rest),
7770        None => (false, t.strip_prefix('+').unwrap_or(t)),
7771    };
7772    let (radix, digits) = match radix {
7773        Some(16) => (
7774            16u32,
7775            digits
7776                .strip_prefix("0x")
7777                .or_else(|| digits.strip_prefix("0X"))
7778                .unwrap_or(digits),
7779        ),
7780        Some(r) => (r, digits),
7781        None => {
7782            if let Some(hex) = digits
7783                .strip_prefix("0x")
7784                .or_else(|| digits.strip_prefix("0X"))
7785            {
7786                (16, hex)
7787            } else {
7788                (10, digits)
7789            }
7790        }
7791    };
7792    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7793    if valid.is_empty() {
7794        return f64::NAN;
7795    }
7796    // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7797    // digits and the error was mapped to `NaN`, so
7798    // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7799    // asks for the mathematical value rounded to a Number, which is what
7800    // repeated multiply-accumulate in `f64` produces.
7801    let n = if radix == 10 {
7802        // Rust's decimal float parser is correctly rounded; digit-by-digit
7803        // multiply-accumulate is not, and drifted a ULP on long inputs
7804        // (`parseInt("999999999999999999999999")` came out
7805        // 1.0000000000000003e+24 rather than 1e+24).
7806        valid.parse::<f64>().unwrap_or(f64::NAN)
7807    } else {
7808        let mut n = 0.0f64;
7809        for c in valid.chars() {
7810            n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7811        }
7812        n
7813    };
7814    if neg {
7815        -n
7816    } else {
7817        n
7818    }
7819}
7820
7821/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7822fn parse_float(args: &[Value]) -> Result<f64, String> {
7823    let sv = host::to_string_value(&arg0(args))?;
7824    Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7825}
7826
7827fn parse_float_str(s: &str) -> f64 {
7828    let t = crate::utf16::js_trim_start(s);
7829    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7830    let inf_body = t
7831        .strip_prefix('+')
7832        .or_else(|| t.strip_prefix('-'))
7833        .unwrap_or(t);
7834    if inf_body.starts_with("Infinity") {
7835        return if t.starts_with('-') {
7836            f64::NEG_INFINITY
7837        } else {
7838            f64::INFINITY
7839        };
7840    }
7841    // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7842    // not the same as the longest run of characters that could appear in one:
7843    // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7844    // only valid once a digit follows `e`. Tracking `end` at every character
7845    // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7846    // came back NaN.
7847    let mut end = 0;
7848    let bytes = t.as_bytes();
7849    let mut seen_dot = false;
7850    let mut seen_e = false;
7851    let mut digits_before_dot = false;
7852    for (i, &c) in bytes.iter().enumerate() {
7853        match c {
7854            b'0'..=b'9' => {
7855                if !seen_dot && !seen_e {
7856                    digits_before_dot = true;
7857                }
7858                end = i + 1;
7859            }
7860            // A sign is only meaningful leading, or straight after the exponent
7861            // marker; it never completes a literal on its own.
7862            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7863            // `1.` is a complete literal; a bare `.` is not.
7864            b'.' if !seen_dot && !seen_e => {
7865                seen_dot = true;
7866                if digits_before_dot {
7867                    end = i + 1;
7868                }
7869            }
7870            b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7871            _ => break,
7872        }
7873    }
7874    if end == 0 {
7875        return f64::NAN;
7876    }
7877    t[..end].parse::<f64>().unwrap_or(f64::NAN)
7878}
7879
7880/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7881/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7882/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7883/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7884/// rather than 1.
7885pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7886    if exp == 0.0 {
7887        return 1.0;
7888    }
7889    if base.is_nan() || exp.is_nan() {
7890        return f64::NAN;
7891    }
7892    if base.abs() == 1.0 && exp.is_infinite() {
7893        return f64::NAN;
7894    }
7895    base.powf(exp)
7896}
7897
7898fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7899    // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7900    // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7901    // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7902    // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7903    // exception: it never reads an argument, so `Math.random(1n)` is fine.
7904    // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7905    // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7906    // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7907    // the host itself and cannot run inside another borrow.
7908    let is_bigint = |a: &Value| {
7909        if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7910            return true;
7911        }
7912        match wrapped_primitive(a) {
7913            Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7914            None => false,
7915        }
7916    };
7917    if fname != "random" && args.iter().any(is_bigint) {
7918        return Err(host::type_error(
7919            "Cannot convert a BigInt value to a number",
7920        ));
7921    }
7922    // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7923    // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7924    // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7925    // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7926    // variadic, and coercing only the first few silently DROPPED the rest —
7927    // `Math.max(...gen)` over five values answered for four of them.
7928    let mut coerced = Vec::with_capacity(args.len());
7929    for a in args {
7930        if matches!(a, Value::Undef) {
7931            coerced.push(a.clone());
7932            continue;
7933        }
7934        let p = host::to_primitive(a, "number")?;
7935        coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7936    }
7937    let args: &[Value] = &coerced;
7938    let x = arg_num(args, 0);
7939    let r = match fname {
7940        "floor" => x.floor(),
7941        "ceil" => x.ceil(),
7942        // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7943        // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7944        // before the floor sees it, so it answers 1 for the largest double below
7945        // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7946        // perturbs integers above 2^52, where `x + 0.5` is no longer
7947        // representable (`Math.round(4503599627370497)` must be the input).
7948        // Splitting the zero-band cases out first also carries the signed zero
7949        // the spec asks for without a post-hoc patch.
7950        "round" => {
7951            if !x.is_finite() || x == 0.0 {
7952                x
7953            } else if x > 0.0 && x < 0.5 {
7954                0.0
7955            } else if (-0.5..0.0).contains(&x) {
7956                -0.0
7957            } else {
7958                // |x| >= 0.5, so `floor` and the subtraction are both exact
7959                // (every double >= 2^52 is already an integer and yields 0 here).
7960                let f = x.floor();
7961                if x - f >= 0.5 {
7962                    f + 1.0
7963                } else {
7964                    f
7965                }
7966            }
7967        }
7968        "trunc" => x.trunc(),
7969        "abs" => x.abs(),
7970        "sign" => {
7971            if x.is_nan() {
7972                f64::NAN
7973            } else if x > 0.0 {
7974                1.0
7975            } else if x < 0.0 {
7976                -1.0
7977            } else {
7978                x
7979            }
7980        }
7981        "sqrt" => x.sqrt(),
7982        "cbrt" => x.cbrt(),
7983        "exp" => x.exp(),
7984        "log" => x.ln(),
7985        "log2" => x.log2(),
7986        "log10" => x.log10(),
7987        "sin" => x.sin(),
7988        "cos" => x.cos(),
7989        "tan" => x.tan(),
7990        "asin" => x.asin(),
7991        "acos" => x.acos(),
7992        "atan" => x.atan(),
7993        "atan2" => x.atan2(arg_num(args, 1)),
7994        // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7995        // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7996        // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7997        // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7998        // clause is shared.
7999        "pow" => js_pow(x, arg_num(args, 1)),
8000        // Hyperbolics and the two precision-preserving log/exp forms.
8001        "sinh" => x.sinh(),
8002        "cosh" => x.cosh(),
8003        "tanh" => x.tanh(),
8004        "asinh" => x.asinh(),
8005        "acosh" => x.acosh(),
8006        "atanh" => x.atanh(),
8007        "log1p" => x.ln_1p(),
8008        "expm1" => x.exp_m1(),
8009        // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
8010        // wraparound, reinterpret as a signed 32-bit result.
8011        "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
8012        "hypot" => {
8013            // Scale by the largest magnitude before squaring — this avoids the
8014            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
8015            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
8016            let mut max = 0.0f64;
8017            for x in &xs {
8018                if x.abs() > max {
8019                    max = x.abs();
8020                }
8021            }
8022            if xs.iter().any(|x| x.is_infinite()) {
8023                f64::INFINITY
8024            } else if max == 0.0 || !max.is_finite() {
8025                max
8026            } else {
8027                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
8028                max * s.sqrt()
8029            }
8030        }
8031        "random" => pseudo_random(),
8032        "max" => {
8033            if args.is_empty() {
8034                f64::NEG_INFINITY
8035            } else {
8036                let mut m = f64::NEG_INFINITY;
8037                for a in args {
8038                    let n = with_host(|h| h.to_number(a));
8039                    if n.is_nan() {
8040                        return Ok(Value::Float(f64::NAN));
8041                    }
8042                    // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
8043                    // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
8044                    // must not keep the -0 the first iteration installed.
8045                    if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
8046                        m = n;
8047                    }
8048                }
8049                m
8050            }
8051        }
8052        "min" => {
8053            if args.is_empty() {
8054                f64::INFINITY
8055            } else {
8056                let mut m = f64::INFINITY;
8057                for a in args {
8058                    let n = with_host(|h| h.to_number(a));
8059                    if n.is_nan() {
8060                        return Ok(Value::Float(f64::NAN));
8061                    }
8062                    // Mirror of `max`: -0 ranks below +0 even though `<` says
8063                    // they are equal, so `Math.min(0, -0)` is -0.
8064                    if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8065                        m = n;
8066                    }
8067                }
8068                m
8069            }
8070        }
8071        // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8072        "clz32" => {
8073            let u = if x.is_finite() {
8074                x.trunc().rem_euclid(4294967296.0) as u32
8075            } else {
8076                0
8077            };
8078            u.leading_zeros() as f64
8079        }
8080        // Round to the nearest single-precision float.
8081        "fround" => (x as f32) as f64,
8082        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8083    };
8084    Ok(Value::Float(r))
8085}
8086
8087/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8088/// Node by nature; kept simple).
8089fn pseudo_random() -> f64 {
8090    use std::cell::Cell;
8091    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8092    SEED.with(|s| {
8093        let mut x = s.get();
8094        x ^= x << 13;
8095        x ^= x >> 7;
8096        x ^= x << 17;
8097        s.set(x);
8098        (x >> 11) as f64 / (1u64 << 53) as f64
8099    })
8100}
8101
8102// ── Object.* ──────────────────────────────────────────────────────────────────
8103
8104/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8105/// properties (10.4.3 `StringExoticObject`).
8106///
8107/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8108/// an exotic object whose own keys are its code-unit indices plus `length`;
8109/// this is the descriptor half of that. (The KEY half lives in
8110/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8111/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8112///
8113/// A boxed `String` object is deliberately NOT routed here: it can carry
8114/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8115/// existing path already reports them alongside the indices.
8116fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8117    // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8118    // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8119    // never catches one.
8120    let s = match v {
8121        Value::Str(s) => (**s).clone(),
8122        _ => with_host(|h| match h.get(v) {
8123            Some(JsObj::Str(s)) => Some(s.clone()),
8124            _ => None,
8125        })?,
8126    };
8127    let units = crate::utf16::Units::of(&s);
8128    Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8129}
8130
8131fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8132    let v = arg0(&args);
8133    require_object_coercible(&v)?;
8134    // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8135    // reports every own STRING key the trap named; the enumerating modes
8136    // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8137    if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8138        if mode == 3 {
8139            let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8140            return Ok(with_host(|h| {
8141                let out: Vec<Value> = keys
8142                    .into_iter()
8143                    .filter(|k| !host::is_symbol_key(k))
8144                    .map(|k| h.new_str(k))
8145                    .collect();
8146                h.new_array(out)
8147            }));
8148        }
8149        // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8150        // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8151        // Going through `own_enum_entries` fired the `get` trap once per key, so
8152        // the observable trap sequence carried a trailing `get` node does not
8153        // emit, and a trap with side effects ran when it should not have.
8154        if mode == 0 {
8155            let keys = crate::proxy::own_enum_string_keys(&v)?;
8156            return Ok(with_host(|h| {
8157                let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8158                h.new_array(out)
8159            }));
8160        }
8161        let entries = crate::proxy::own_enum_entries(&v)?;
8162        return Ok(with_host(|h| {
8163            let out: Vec<Value> = entries
8164                .into_iter()
8165                .map(|(k, val)| match mode {
8166                    0 => h.new_str(k),
8167                    1 => val,
8168                    _ => {
8169                        let ks = h.new_str(k);
8170                        h.new_array(vec![ks, val])
8171                    }
8172                })
8173                .collect();
8174            h.new_array(out)
8175        }));
8176    }
8177    // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8178    // .prototype`, `String.prototype`, the error hierarchy — answers from the
8179    // generated table too. It was answering from its own property map instead,
8180    // which carries neither the right names nor V8's order: `Symbol.prototype`
8181    // reported `toLocaleString` and omitted `description`, and
8182    // `String.prototype` omitted `length` and every Annex B HTML method.
8183    //
8184    // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8185    // the two representations of a prototype cannot answer differently.
8186    let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8187        .filter(|ns| intrinsic_proto_members(ns).is_some());
8188    // A builtin prototype namespace that exposes enumerable methods for copying
8189    // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8190    if let Some(ns) = real_proto_ns.or_else(|| {
8191        with_host(|h| match h.get(&v) {
8192            Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8193            _ => None,
8194        })
8195    }) {
8196        // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8197        // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8198        // interface, which the table records per name.
8199        if let Some(members) = intrinsic_proto_members(&ns) {
8200            let ctor = ns.trim_end_matches(".prototype");
8201            let mut names: Vec<String> = members
8202                .iter()
8203                .filter(|m| mode == 3 || m.starts_with('+'))
8204                .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8205                // `getOwnPropertyNames` reports STRING keys only; the table's
8206                // `@@` entries are symbol-keyed members and belong to
8207                // `getOwnPropertySymbols` instead.
8208                .filter(|m| !m.starts_with("@@"))
8209                .collect();
8210            // Plus whatever a script patched onto this prototype under a NEW
8211            // name — an ordinary enumerable own property, so it lists in every
8212            // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8213            // an assignment that `Array.prototype.patch` read back happily.
8214            //
8215            // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8216            // that member's attributes alone: restoring a saved `join` must not
8217            // turn it into an enumerable key.
8218            for k in with_host(|h| h.builtin_static_keys(&ns)) {
8219                if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8220                    names.push(k);
8221                }
8222            }
8223            return Ok(with_host(|h| {
8224                let out: Vec<Value> = names
8225                    .iter()
8226                    .map(|name| {
8227                        // An accessor member has no thunk — `Map.prototype.size`
8228                        // is not a function — so a VALUE read of one answers
8229                        // undefined rather than synthesizing a callable.
8230                        let val = |h: &mut host::JsHost| {
8231                            if let Some(v) = h.builtin_static(&ns, name) {
8232                                return v;
8233                            }
8234                            let key = format!("@proto:{ctor}:{name}");
8235                            if builtin_meta(&key).is_some() {
8236                                h.alloc(JsObj::Builtin(key))
8237                            } else {
8238                                Value::Undef
8239                            }
8240                        };
8241                        match mode {
8242                            1 => val(h),
8243                            2 => {
8244                                let ks = h.new_str(name.clone());
8245                                let v = val(h);
8246                                h.new_array(vec![ks, v])
8247                            }
8248                            _ => h.new_str(name.clone()),
8249                        }
8250                    })
8251                    .collect();
8252                h.new_array(out)
8253            }));
8254        }
8255        if let Some(names) = builtin_proto_method_names(&ns) {
8256            return Ok(with_host(|h| {
8257                let out: Vec<Value> = names
8258                    .iter()
8259                    .map(|name| match mode {
8260                        1 => h.alloc(JsObj::Builtin(format!(
8261                            "@proto:{}:{name}",
8262                            ns.trim_end_matches(".prototype")
8263                        ))),
8264                        2 => {
8265                            let ks = h.new_str(*name);
8266                            let val = h.alloc(JsObj::Builtin(format!(
8267                                "@proto:{}:{name}",
8268                                ns.trim_end_matches(".prototype")
8269                            )));
8270                            h.new_array(vec![ks, val])
8271                        }
8272                        _ => h.new_str(*name),
8273                    })
8274                    .collect();
8275                h.new_array(out)
8276            }));
8277        }
8278        // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8279        // keys are the members node-js implements, each resolved to the same
8280        // first-class value a property read would give.
8281        let mut names = crate::stdlib::namespace_keys(&ns);
8282        // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8283        // its members live in the builtin dispatch table. They are
8284        // non-enumerable in V8, so they surface only under
8285        // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8286        if names.is_empty() && mode == 3 {
8287            let prefix = format!("{ns}.");
8288            // A builtin constructor's own `length`/`name`/`prototype` come
8289            // first, as they do in V8.
8290            if is_builtin_ctor(&ns) {
8291                names.extend(["length", "name", "prototype"].map(str::to_string));
8292            }
8293            names.extend(
8294                NS_METHODS
8295                    .iter()
8296                    .filter_map(|q| q.strip_prefix(&prefix))
8297                    .map(|m| m.to_string()),
8298            );
8299            // The numeric constants are members too. Without them
8300            // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8301            // actually answers — the eight it dropped being `PI` and its
8302            // siblings, which read fine and now own a descriptor as well.
8303            names.extend(
8304                namespace_constants(&ns)
8305                    .iter()
8306                    .map(|(k, _)| (*k).to_string()),
8307            );
8308        }
8309        // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8310        // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8311        // was `[]`, which said the function had no properties at all while both
8312        // of them read back a value. `length` is listed only where the intrinsic
8313        // table has an arity, so the names never advertise a read that answers
8314        // `undefined`.
8315        if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8316            if builtin_meta(&ns).is_some() {
8317                names.push("length".to_string());
8318            }
8319            names.push("name".to_string());
8320        }
8321        // Whatever a script assigned onto the namespace, in assignment order and
8322        // after the built-in members — an ordinary enumerable own property, so
8323        // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8324        // missing from every listing, which made a patched prototype read as
8325        // unpatched to any code that enumerates rather than reads.
8326        for k in with_host(|h| h.builtin_static_keys(&ns)) {
8327            if !names.contains(&k) {
8328                names.push(k);
8329            }
8330        }
8331        if !names.is_empty() {
8332            let entries: Vec<(String, Value)> = names
8333                .into_iter()
8334                .map(|k| {
8335                    let val = namespace_property(&ns, &k);
8336                    (k, val)
8337                })
8338                .collect();
8339            return Ok(with_host(|h| {
8340                let out: Vec<Value> = entries
8341                    .into_iter()
8342                    .map(|(k, val)| match mode {
8343                        1 => val,
8344                        2 => {
8345                            let ks = h.new_str(k);
8346                            h.new_array(vec![ks, val])
8347                        }
8348                        _ => h.new_str(k),
8349                    })
8350                    .collect();
8351                h.new_array(out)
8352            }));
8353        }
8354    }
8355    // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8356    // non-enumerable ones, plus the exotic `length` an array carries.
8357    let entries: Vec<(String, Value)> = with_host(|h| {
8358        if mode == 3 {
8359            // An array's exotic `length` is already placed (after the indices,
8360            // before the ordinary string keys) by `own_key_names`.
8361            return h
8362                .own_key_names(&v, false)
8363                .into_iter()
8364                .map(|k| (k, Value::Undef))
8365                .collect();
8366        }
8367        Vec::new()
8368    });
8369    // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8370    // key/value pairs, so asking it for them ran every enumerable getter —
8371    // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8372    // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8373    // side effects:
8374    //
8375    //     let n = 0; const o = { get g() { n++; return 1 } };
8376    //     Object.keys(o); n   // was 1, node says 0
8377    //
8378    // `values`/`entries` (modes 1 and 2) do read, and still do.
8379    let entries = match mode {
8380        3 => entries,
8381        0 => with_host(|h| h.own_enum_key_names(&v))
8382            .into_iter()
8383            .map(|k| (k, Value::Undef))
8384            .collect(),
8385        _ => host::own_enum_entries_deep(&v)?,
8386    };
8387    Ok(with_host(|h| {
8388        let out: Vec<Value> = entries
8389            .into_iter()
8390            .map(|(k, val)| match mode {
8391                0 | 3 => h.new_str(k),
8392                1 => val,
8393                _ => {
8394                    let ks = h.new_str(k);
8395                    h.new_array(vec![ks, val])
8396                }
8397            })
8398            .collect();
8399        h.new_array(out)
8400    }))
8401}
8402
8403fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8404    let target = arg0(&args);
8405    // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8406    // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8407    require_object_coercible(&target)?;
8408    for src in args.iter().skip(1) {
8409        // `Object.assign` copies own *enumerable* properties, running any getter
8410        // — symbol-keyed ones included (7.3.25).
8411        let entries = host::own_enum_entries_deep(src)?;
8412        let syms = with_host(|h| h.own_symbol_entries(src));
8413        // A plain object target is filled in place (one borrow, then a single
8414        // re-canonicalization of the integer-index keys).
8415        let filled = with_host(|h| {
8416            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8417                for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8418                    p.insert(k, v);
8419                }
8420                host::canonicalize_own_keys(p);
8421                return true;
8422            }
8423            false
8424        });
8425        // Any OTHER target — an array being the common one — goes through the
8426        // ordinary Set path. The in-place branch above matched `JsObj::Object`
8427        // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8428        // returned the untouched array: no error, just a missing property. The
8429        // Set path is what an `arr.extra = 9` assignment already used, so index
8430        // and non-index keys land where they do for a direct write.
8431        if !filled {
8432            for (k, v) in entries.into_iter().chain(syms) {
8433                set_property(&target, &k, v)?;
8434            }
8435        }
8436    }
8437    Ok(target)
8438}
8439
8440fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8441    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8442    let mut props: IndexMap<String, Value> = IndexMap::new();
8443    for p in pairs {
8444        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8445        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8446        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8447        props.insert(key, val);
8448    }
8449    Ok(with_host(|h| h.new_object(props)))
8450}
8451
8452/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8453/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8454/// members mapped to that key, in first-seen key order.
8455fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8456    group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8457    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8458    let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8459    // Stepped, not drained: the callback runs per element, so a throwing one
8460    // stops at the first. Draining first meant an infinite source never reached
8461    // the callback at all and the call HUNG.
8462    host::iter_for_each(&arg0(&args), |item, i| {
8463        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8464        let key = with_host(|h| h.property_key(&key_v));
8465        groups.entry(key).or_default().push(item);
8466        Ok(())
8467    })?;
8468    let props: IndexMap<String, Value> = with_host(|h| {
8469        groups
8470            .into_iter()
8471            .map(|(k, v)| (k, h.new_array(v)))
8472            .collect()
8473    });
8474    let obj = with_host(|h| h.new_object(props));
8475    // A null-prototype object (as Node returns), so it has no inherited members.
8476    with_host(|h| {
8477        let nv = h.null();
8478        h.set_proto(&obj, nv);
8479    });
8480    Ok(obj)
8481}
8482
8483/// The `groupBy` family words a non-iterable argument its OWN way — a third
8484/// vocabulary, alongside the array-literal spread's and the call spread's:
8485///
8486/// ```text
8487/// null / undefined   "<Name> called on null or undefined"
8488/// anything else      "<typeof> [value ]is not iterable (cannot read property
8489///                     Symbol(Symbol.iterator))"
8490/// ```
8491///
8492/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8493/// string and a boolean name the value too.
8494fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8495    if with_host(|h| h.is_nullish(v)) {
8496        return Err(host::type_error(&format!(
8497            "{name} called on null or undefined"
8498        )));
8499    }
8500    // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8501    // here, so the stepping loop below then saw an exhausted one — the finite
8502    // case returned an empty group and the infinite case was back to hanging.
8503    let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8504    if with_host(|h| host::is_callable(h, &iter_fn)) {
8505        return Ok(());
8506    }
8507    Err(host::type_error(&not_iterable_typed(v)))
8508}
8509
8510/// The `<type> <value> is not iterable (cannot read property
8511/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8512/// no name to report: a plain object, a symbol and a bigint name only their
8513/// TYPE; a number, a string and a boolean name the value too.
8514pub(crate) fn not_iterable_typed(v: &Value) -> String {
8515    let shown = with_host(|h| {
8516        let kind = h.type_of(v);
8517        match kind {
8518            "object" | "symbol" | "bigint" => kind.to_string(),
8519            "string" => format!("string \"{}\"", h.str_of(v)),
8520            _ => format!("{kind} {}", h.str_of(v)),
8521        }
8522    });
8523    format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8524}
8525
8526/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8527/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8528fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8529    group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8530    let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8531    let m = with_host(|h| {
8532        h.alloc(JsObj::Map {
8533            entries: IndexMap::new(),
8534            weak: false,
8535        })
8536    });
8537    // Stepped for the same reason `Object.groupBy` is.
8538    host::iter_for_each(&arg0(&args), |item, i| {
8539        let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8540        let existing = map_method(&m, "get", vec![key_v.clone()])?;
8541        if matches!(existing, Value::Undef) {
8542            let arr = with_host(|h| h.new_array(vec![item]));
8543            map_method(&m, "set", vec![key_v, arr])?;
8544        } else {
8545            with_host(|h| {
8546                if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8547                    a.push(item);
8548                }
8549            });
8550        }
8551        Ok(())
8552    })?;
8553    Ok(m)
8554}
8555
8556/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8557/// element and each `mapFn` result.
8558///
8559/// Written in JavaScript and compiled once, because the operation IS an async
8560/// function: a Rust builtin runs outside any coroutine and has no way to await,
8561/// so draining a promise from there would mean running the microtask queue by
8562/// hand. Delegating to the engine's own `async`/`for await` keeps the
8563/// suspension semantics — and the ordering they imply — exactly the language's.
8564///
8565/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8566/// array-like. Everything iterable goes through `for await`, which awaits a sync
8567/// source's elements individually — that is what makes
8568/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8569/// accepted because an async generator object does not expose
8570/// `Symbol.asyncIterator` on this frontend.
8571fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8572    thread_local! {
8573        static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8574    }
8575    const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8576        const out = []; let i = 0;\n\
8577        const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8578        const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8579            || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8580        if (iterable) {\n\
8581            for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8582            return out;\n\
8583        }\n\
8584        const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8585        while (i < len) { await step(items[i]); }\n\
8586        return out;\n\
8587    })";
8588    let f = IMPL.with(|c| c.borrow().clone());
8589    let f = match f {
8590        Some(f) => f,
8591        None => {
8592            let f = crate::eval_in_global_scope(SRC)?;
8593            IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8594            f
8595        }
8596    };
8597    host::invoke(&f, args, None)
8598}
8599
8600fn array_from(args: Vec<Value>) -> Result<Value, String> {
8601    // `Array.from` accepts generators and user iterables, plus array-likes with a
8602    // numeric `.length`.
8603    let src = arg0(&args);
8604    if let Some(cb) = args.get(1).cloned() {
8605        // Stepped, not drained: the mapper runs per element as the iterator
8606        // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8607        // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8608        // all and HUNG, and a throwing mapper could not close the iterator.
8609        let this = this_arg(&args, 2);
8610        let mut out = Vec::new();
8611        let mapped = host::iter_for_each(&src, |v, i| {
8612            out.push(host::invoke(
8613                &cb,
8614                vec![v, Value::Float(i as f64)],
8615                this.clone(),
8616            )?);
8617            Ok(())
8618        });
8619        match mapped {
8620            Ok(()) => {}
8621            // An array-LIKE has no iterator; fall back to its indexed items.
8622            Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8623                out.clear();
8624                for (i, it) in array_like_items(&src).into_iter().enumerate() {
8625                    out.push(host::invoke(
8626                        &cb,
8627                        vec![it, Value::Float(i as f64)],
8628                        this.clone(),
8629                    )?);
8630                }
8631            }
8632            Err(e) => return Err(e),
8633        }
8634        return construct_array_like(host::current_static_this(), out);
8635    }
8636    let items = match host::iter_all(&src) {
8637        Ok(v) => v,
8638        Err(_) => array_like_items(&src),
8639    };
8640    // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8641    // result is an instance of it. It always allocated a plain array, which is
8642    // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8643    // never started.
8644    construct_array_like(host::current_static_this(), items)
8645}
8646
8647/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8648pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8649    // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8650    // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8651    // because the infallible read does no `ToPrimitive`. A throw from it is
8652    // swallowed here for the same reason the `length` read is: this helper has
8653    // no way to report one, and every caller treats an unreadable length as 0.
8654    let len = get_property(src, "length")
8655        .ok()
8656        .and_then(|l| host::to_primitive(&l, "number").ok())
8657        .map(|l| with_host(|h| h.to_number(&l)))
8658        .unwrap_or(0.0);
8659    if !len.is_finite() || len <= 0.0 {
8660        return Vec::new();
8661    }
8662    (0..len as usize)
8663        .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8664        .collect()
8665}
8666
8667// ── JSON ──────────────────────────────────────────────────────────────────────
8668
8669fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8670    // A CALLABLE second argument is the replacer function, and it is checked
8671    // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8672    // callable never also reaches the key-filter path below.
8673    let replacer = args
8674        .get(1)
8675        .filter(|r| with_host(|h| host::is_callable(h, r)))
8676        .cloned();
8677    // `toJSON` and the replacer run BEFORE serialization and are user code, so
8678    // the tree is rewritten first — outside the host borrow `json_str` holds,
8679    // and before the BigInt walk, which has no cycle guard of its own.
8680    //
8681    // The top-level value is a property of a synthetic wrapper `{ "": value }`
8682    // under key `""`, which is exactly the holder the replacer receives as
8683    // `this` on its first call.
8684    let root = arg0(&args);
8685    let wrapper = with_host(|h| {
8686        let mut m: IndexMap<String, Value> = IndexMap::new();
8687        m.insert(String::new(), root.clone());
8688        h.new_object(m)
8689    });
8690    let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8691    // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8692    // bigint form), matching Node's exact message.
8693    if with_host(|h| json_has_bigint(h, &v)) {
8694        return Err(host::type_error("Do not know how to serialize a BigInt"));
8695    }
8696    let indent = match args.get(2) {
8697        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8698        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8699        None => String::new(),
8700    };
8701    // A replacer array (args[1]) restricts which object keys are serialized.
8702    let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8703        with_host(|h| match h.get(r) {
8704            Some(JsObj::Array(items)) => {
8705                Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8706            }
8707            _ => None,
8708        })
8709    });
8710    let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8711    match s {
8712        Some(s) => Ok(with_host(|h| h.new_str(s))),
8713        None => Ok(Value::Undef),
8714    }
8715}
8716
8717/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8718/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8719/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8720/// object survives. Applies to user methods, class methods, and the native
8721/// `Date`/`Buffer`/`URL` accessors alike.
8722///
8723/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8724/// objects currently being walked so a cyclic structure is reported rather than
8725/// spinning forever.
8726///
8727/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8728/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8729/// because the inner method is a plain (unserializable) function property of the
8730/// returned object, not a second conversion hook.
8731fn apply_to_json(
8732    holder: &Value,
8733    key: &str,
8734    v: &Value,
8735    path: &mut JsonPath,
8736    rep: Option<&Value>,
8737) -> Result<Value, String> {
8738    let mut v = v.clone();
8739    if matches!(v, Value::Obj(_)) {
8740        let tag = crate::stdlib::native_tag(&v);
8741        // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8742        // supplies one through its `get` trap. `lookup_chain` walks the
8743        // property map and never asks the handler, so a proxy carrying a
8744        // `toJSON` was serialized as a plain object instead of by its own
8745        // method — and node's trap log starts with that `get`.
8746        let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8747            get_property(&v, "toJSON")?
8748        } else {
8749            with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8750        };
8751        let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8752            || tag
8753                .as_deref()
8754                .map(crate::stdlib::has_to_json)
8755                .unwrap_or(false);
8756        if has_to_json {
8757            let k = with_host(|h| h.new_str(key.to_string()));
8758            v = host::call_method(&v, "toJSON", vec![k])?;
8759        }
8760    }
8761    if let Some(rep) = rep {
8762        let k = with_host(|h| h.new_str(key.to_string()));
8763        v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8764    }
8765    // How `v` was reached from its holder, as V8 names the step in a
8766    // circular-structure message: `index 1` under an array, else `property 'k'`.
8767    let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8768        format!("index {key}")
8769    } else {
8770        format!("property '{key}'")
8771    };
8772    json_walk_children(&v, path, &via, rep)
8773}
8774
8775/// The objects `JSON.stringify` is inside of, outermost first, each with the
8776/// step that reached it from its holder (`property 'x'` / `index 1`).
8777type JsonPath = Vec<(String, Value)>;
8778
8779/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8780/// starts at to the key that closes it. At most the first two and the last one
8781/// intermediate step are listed, with `|     ...` standing for the rest.
8782fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8783    const PREFIX: usize = 2;
8784    const POSTFIX: usize = 1;
8785    let ctor = |v: &Value| -> String {
8786        with_host(|h| match h.get(v) {
8787            Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8788            _ => match h.ctor_name(v) {
8789                n if n.is_empty() => "Object".to_string(),
8790                n => n,
8791            },
8792        })
8793    };
8794    let line = |i: usize| {
8795        format!(
8796            "\n    |     {} -> object with constructor '{}'",
8797            path[i].0,
8798            ctor(&path[i].1)
8799        )
8800    };
8801    let mut msg = format!(
8802        "Converting circular structure to JSON\n    --> starting at object with constructor '{}'",
8803        ctor(&path[start].1)
8804    );
8805    let prefix_end = path.len().min(start + 1 + PREFIX);
8806    for i in start + 1..prefix_end {
8807        msg.push_str(&line(i));
8808    }
8809    if path.len() > prefix_end + POSTFIX {
8810        msg.push_str("\n    |     ...");
8811    }
8812    for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8813        msg.push_str(&line(i));
8814    }
8815    msg.push_str(&format!("\n    --- {closing} closes the circle"));
8816    msg
8817}
8818
8819/// Whether a raw property key of a host object is one `json_str` serializes. The
8820/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8821/// invisible to JSON, so the replacer must not be invoked for them either.
8822fn json_visible_key(k: &str) -> bool {
8823    !k.starts_with("@@") && !k.starts_with('#')
8824}
8825
8826/// Recurse into the elements/properties of an already-converted value, running
8827/// `apply_to_json` for each with this value as the holder.
8828fn json_walk_children(
8829    v: &Value,
8830    path: &mut JsonPath,
8831    via: &str,
8832    rep: Option<&Value>,
8833) -> Result<Value, String> {
8834    if !matches!(v, Value::Obj(_)) {
8835        return Ok(v.clone());
8836    }
8837    // A value that contains itself has no JSON form.
8838    if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8839        return Err(host::type_error(&circular_json_message(path, start, via)));
8840    }
8841    // A Proxy owns no property map, so it is snapshotted through its traps into
8842    // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8843    // — which read every member through `[[Get]]`, exactly as the snapshot does.
8844    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8845        let snap = crate::proxy::json_snapshot(v)?;
8846        path.push((via.to_string(), v.clone()));
8847        let out = json_walk_children(&snap, path, via, rep);
8848        path.pop();
8849        return out;
8850    }
8851    let obj = with_host(|h| h.get(v).cloned());
8852    path.push((via.to_string(), v.clone()));
8853    let out = (|| match obj {
8854        Some(JsObj::Array(items)) => {
8855            // Read the elements through the accessor-aware funnel: an index
8856            // with a getter must be SERIALIZED as what the getter returns, and
8857            // the backing vector still holds the stale slot.
8858            let mut resolved = items;
8859            // An index with a getter must be SERIALIZED as what the getter
8860            // returns, and it also forces a rebuild below: keeping the original
8861            // array would hand the serializer back the stale backing vector.
8862            let had_accessor = resolve_index_accessors(v, &mut resolved);
8863            let items = resolved;
8864            let mut out = Vec::with_capacity(items.len());
8865            let mut changed = had_accessor;
8866            for (i, it) in items.iter().enumerate() {
8867                let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8868                changed |= !with_host(|h| h.strict_eq(&nv, it));
8869                out.push(nv);
8870            }
8871            // Keep identity when nothing changed, so an enclosing object is not
8872            // needlessly rebuilt (which would drop its property attributes).
8873            if changed {
8874                Ok(with_host(|h| h.new_array(out)))
8875            } else {
8876                Ok(v.clone())
8877            }
8878        }
8879        Some(JsObj::Object(props)) => {
8880            // An enumerable own accessor must have its getter RUN and the result
8881            // serialized. That cannot happen inside `json_str` (which holds the
8882            // host borrow), so materialize here — the same reason `toJSON` is
8883            // applied in this pass.
8884            let has_accessor = with_host(|h| {
8885                h.own_accessor_keys(v)
8886                    .iter()
8887                    .any(|k| h.prop_attrs(v, k).enumerable)
8888            });
8889            if has_accessor {
8890                let mut next: IndexMap<String, Value> = IndexMap::new();
8891                for (k, val) in host::own_enum_entries_deep(v)? {
8892                    let nv = if json_visible_key(&k) {
8893                        apply_to_json(v, &k, &val, path, rep)?
8894                    } else {
8895                        val
8896                    };
8897                    next.insert(k, nv);
8898                }
8899                return Ok(with_host(|h| h.new_object(next)));
8900            }
8901            // Only rebuild when a descendant actually changed, so plain data keeps
8902            // its identity (and its prototype / native tag).
8903            let mut next: IndexMap<String, Value> = IndexMap::new();
8904            let mut changed = false;
8905            for (k, val) in &props {
8906                let nv = if json_visible_key(k) {
8907                    apply_to_json(v, k, val, path, rep)?
8908                } else {
8909                    val.clone()
8910                };
8911                changed |= !with_host(|h| h.strict_eq(&nv, val));
8912                next.insert(k.clone(), nv);
8913            }
8914            if changed {
8915                Ok(with_host(|h| {
8916                    let o = h.new_object(next);
8917                    h.copy_prop_attrs(v, &o);
8918                    o
8919                }))
8920            } else {
8921                Ok(v.clone())
8922            }
8923        }
8924        _ => Ok(v.clone()),
8925    })();
8926    path.pop();
8927    out
8928}
8929
8930/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8931/// try to serialize (a value in an array/object) — such a value throws.
8932fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8933    match h.get(v) {
8934        Some(JsObj::BigInt(_)) => true,
8935        Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8936        Some(JsObj::Object(props)) => props
8937            .iter()
8938            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8939            .any(|(_, val)| json_has_bigint(h, val)),
8940        _ => false,
8941    }
8942}
8943
8944fn json_str(
8945    h: &host::JsHost,
8946    v: &Value,
8947    indent: &str,
8948    depth: usize,
8949    keys: Option<&[String]>,
8950) -> Option<String> {
8951    let sep = if indent.is_empty() { ":" } else { ": " };
8952    match v {
8953        Value::Undef => None,
8954        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8955        Value::Int(n) => Some(n.to_string()),
8956        Value::Float(f) => Some(if f.is_finite() {
8957            host::fmt_number(*f)
8958        } else {
8959            "null".into()
8960        }),
8961        Value::Str(s) => Some(json_quote(s)),
8962        Value::Obj(_) => match h.get(v) {
8963            Some(JsObj::Str(s)) => Some(json_quote(s)),
8964            Some(JsObj::Null) => Some("null".into()),
8965            // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8966            // whole point of it, and it is why a number wider than a `double`
8967            // can survive a round trip.
8968            _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8969                Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8970                _ => None,
8971            },
8972            // A Map/Set has no ENTRIES to serialize (they are internal slots),
8973            // but any own property a script attached is serialized like an
8974            // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8975            // is `{"a":1}`.
8976            Some(JsObj::Map { .. })
8977            | Some(JsObj::Set { .. })
8978            | Some(JsObj::RegExp(_))
8979            // A Promise and a generator are ORDINARY objects to the serializer:
8980            // their state is internal slots, so they contribute no entries and
8981            // render as `{}`. They were being omitted entirely instead, so a
8982            // promise in an array became `null` and one in an object vanished.
8983            | Some(JsObj::Promise { .. })
8984            | Some(JsObj::Generator { .. }) => {
8985                let parts: Vec<String> = h
8986                    .own_enum_entries(v)
8987                    .into_iter()
8988                    .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8989                    .filter_map(|(k, val)| {
8990                        json_str(h, &val, indent, depth + 1, keys)
8991                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8992                    })
8993                    .collect();
8994                Some(wrap(&parts, "{", "}", indent, depth))
8995            }
8996            // A NON-callable builtin is a namespace object, not a function, so
8997            // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8998            // are all non-enumerable), where omitting it made the whole property
8999            // disappear from its holder.
9000            Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
9001                let parts: Vec<String> = crate::stdlib::namespace_keys(n)
9002                    .into_iter()
9003                    .filter_map(|k| {
9004                        let val = h.builtin_static(n, &k)?;
9005                        json_str(h, &val, indent, depth + 1, keys)
9006                            .map(|s| format!("{}{sep}{s}", json_quote(&k)))
9007                    })
9008                    .collect();
9009                Some(wrap(&parts, "{", "}", indent, depth))
9010            }
9011            // Functions and symbols are omitted (undefined) as values.
9012            Some(JsObj::Func(_))
9013            | Some(JsObj::Builtin(_))
9014            | Some(JsObj::BoundMethod { .. })
9015            | Some(JsObj::BoundFunc { .. })
9016            | Some(JsObj::Class(_))
9017            | Some(JsObj::Symbol { .. }) => None,
9018            Some(JsObj::Array(items)) => {
9019                if items.is_empty() {
9020                    return Some("[]".into());
9021                }
9022                let parts: Vec<String> = items
9023                    .iter()
9024                    .map(|x| {
9025                        json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
9026                    })
9027                    .collect();
9028                Some(wrap(&parts, "[", "]", indent, depth))
9029            }
9030            Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
9031                // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
9032                // the primitive it boxes, not as the object holding it —
9033                // `JSON.stringify(new Number(1))` is `1`, not `{}`.
9034                json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
9035            }
9036            Some(JsObj::Object(props)) => {
9037                // A replacer array restricts (and orders) which keys are emitted.
9038                let parts: Vec<String> = match keys {
9039                    Some(allow) => allow
9040                        .iter()
9041                        .filter_map(|k| {
9042                            props.get(k).and_then(|val| {
9043                                json_str(h, val, indent, depth + 1, keys)
9044                                    .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9045                            })
9046                        })
9047                        .collect(),
9048                    None => h
9049                        .own_enum_entries(v)
9050                        .iter()
9051                        .filter_map(|(k, val)| {
9052                            json_str(h, val, indent, depth + 1, keys)
9053                                .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9054                        })
9055                        .collect(),
9056                };
9057                if parts.is_empty() {
9058                    return Some("{}".into());
9059                }
9060                Some(wrap(&parts, "{", "}", indent, depth))
9061            }
9062            _ => Some("null".into()),
9063        },
9064        _ => Some("null".into()),
9065    }
9066}
9067
9068fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9069    if indent.is_empty() {
9070        format!("{open}{}{close}", parts.join(","))
9071    } else {
9072        let pad = indent.repeat(depth + 1);
9073        let pad_close = indent.repeat(depth);
9074        format!(
9075            "{open}\n{pad}{}\n{pad_close}{close}",
9076            parts.join(&format!(",\n{pad}"))
9077        )
9078    }
9079}
9080
9081fn json_quote(s: &str) -> String {
9082    let mut out = String::from("\"");
9083    for c in s.chars() {
9084        match c {
9085            '"' => out.push_str("\\\""),
9086            '\\' => out.push_str("\\\\"),
9087            '\n' => out.push_str("\\n"),
9088            '\t' => out.push_str("\\t"),
9089            '\r' => out.push_str("\\r"),
9090            // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9091            // Backspace and form feed were missing, so they fell through to the
9092            // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9093            // `""` where node produces `"\b"`. Both parse back to the same
9094            // string, so the difference is invisible to a round trip and shows
9095            // up only as a byte mismatch against a fixture or a checksum.
9096            '\u{8}' => out.push_str("\\b"),
9097            '\u{c}' => out.push_str("\\f"),
9098            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9099            _ => out.push(c),
9100        }
9101    }
9102    out.push('"');
9103    out
9104}
9105
9106fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9107    let s = with_host(|h| h.str_of(&arg0(&args)));
9108    let mut p = JsonParser {
9109        chars: s.chars().collect(),
9110        pos: 0,
9111        prims: Vec::new(),
9112        record: args
9113            .get(1)
9114            .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9115    };
9116    p.skip_ws();
9117    if p.peek().is_none() {
9118        return Err("SyntaxError: Unexpected end of JSON input".into());
9119    }
9120    let v = p.parse_value()?;
9121    let value_end = p.pos;
9122    p.skip_ws();
9123    // Anything after the top-level value is an error — the parser used to accept
9124    // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9125    if let Some(c) = p.peek() {
9126        // V8 names the token kind only when it butts directly against the value
9127        // (`01` -> "Unexpected number at position 1"); with whitespace between
9128        // it is just a non-whitespace character (`1 2`).
9129        // Only a digit butted directly against a completed number literal —
9130        // V8's number scanner is still in number context there. `5"x"` and
9131        // `[0,1]0` exit the scanner cleanly and get the generic message.
9132        let after_number = value_end > 0
9133            && p.pos == value_end
9134            && p.chars[value_end - 1].is_ascii_digit()
9135            && c.is_ascii_digit();
9136        return Err(if after_number {
9137            p.err_at("Unexpected number", p.pos)
9138        } else {
9139            p.err_trailing(p.pos)
9140        });
9141    }
9142    // Optional reviver: walk bottom-up, transforming each (key, value).
9143    if let Some(reviver) = args
9144        .get(1)
9145        .filter(|r| with_host(|h| host::is_callable(h, r)))
9146        .cloned()
9147    {
9148        // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9149        // constructs before the walk.
9150        let root = with_host(|h| {
9151            let mut m: IndexMap<String, Value> = IndexMap::new();
9152            m.insert(String::new(), v.clone());
9153            h.new_object(m)
9154        });
9155        return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9156    }
9157    Ok(v)
9158}
9159
9160/// `JSON.parse` reviver walk: recurse into children first, then call
9161/// `reviver(key, value)`; a returned `undefined` drops the property.
9162///
9163/// The reviver runs with the HOLDER as `this` (25.5.1.1
9164/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9165/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9166/// so `this` was undefined and a reviver could not reach its siblings.
9167/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9168/// VERBATIM, so a number too large for a `double` survives a round trip
9169/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9170///
9171/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9172/// across the whole matrix, is
9173///
9174/// ```text
9175/// ""                 -> SyntaxError: Invalid value for JSON.rawJSON
9176/// leading whitespace -> the parse error for that first character
9177/// a complete literal -> ok
9178/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9179/// a broken literal   -> the parse error the scanner raised
9180/// ```
9181///
9182/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9183/// invalid-value message even though `JSON.parse` accepts the former and gives
9184/// a token error for the latter.
9185fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9186    const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9187    let s = with_host(|h| h.str_of(&arg0(&args)));
9188    if s.is_empty() {
9189        return Err(INVALID.into());
9190    }
9191    let mut p = JsonParser {
9192        chars: s.chars().collect(),
9193        pos: 0,
9194        prims: Vec::new(),
9195        record: false,
9196    };
9197    // An object or an array is rejected where it starts, as leading whitespace
9198    // is — both are "not a primitive", but node reports the token.
9199    if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9200        return Err(p.err_token(0));
9201    }
9202    p.parse_value()?;
9203    if p.pos != p.chars.len() {
9204        // A digit butted against a completed number is still in the number
9205        // scanner, so `"01"` reports the scanner's error rather than leftover
9206        // input — the same distinction `json_parse` draws for trailing text.
9207        if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9208            return Err(p.err_at("Unexpected number", p.pos));
9209        }
9210        return Err(INVALID.into());
9211    }
9212    // A null prototype and one own `rawJSON` property, frozen — the brand is a
9213    // hidden slot so `Object.keys` stays `["rawJSON"]`.
9214    Ok(with_host(|h| {
9215        let mut m: IndexMap<String, Value> = IndexMap::new();
9216        let text = h.new_str(s);
9217        m.insert("rawJSON".into(), text);
9218        let o = h.new_object(m);
9219        let null = h.null();
9220        h.set_proto(&o, null);
9221        h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9222        h.seal_object(&o, true);
9223        o
9224    }))
9225}
9226
9227/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9228/// NOT one, which is why the marker is a hidden slot rather than the property.
9229fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9230    Ok(Value::Bool(is_raw_json(&arg0(&args))))
9231}
9232
9233fn is_raw_json(v: &Value) -> bool {
9234    with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9235}
9236
9237fn json_revive(
9238    key: &str,
9239    val: Value,
9240    reviver: &Value,
9241    holder: &Value,
9242    prims: &[String],
9243    next: &mut usize,
9244) -> Result<Value, String> {
9245    // A PRIMITIVE claims the next recorded source slice before its children
9246    // would — it has none — and a container claims nothing. The walk descends in
9247    // the same order the parse produced them, so one cursor lines the two up.
9248    let is_container =
9249        with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9250    let source = if !is_container {
9251        let s = prims.get(*next).cloned();
9252        if s.is_some() {
9253            *next += 1;
9254        }
9255        s
9256    } else {
9257        None
9258    };
9259    match with_host(|h| h.get(&val).cloned()) {
9260        Some(JsObj::Array(items)) => {
9261            for i in 0..items.len() {
9262                let elem = with_host(|h| match h.get(&val) {
9263                    Some(JsObj::Array(it)) => it[i].clone(),
9264                    _ => Value::Undef,
9265                });
9266                let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9267                with_host(|h| {
9268                    if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9269                        it[i] = nv;
9270                    }
9271                });
9272            }
9273        }
9274        Some(JsObj::Object(props)) => {
9275            let keys: Vec<String> = props
9276                .keys()
9277                .filter(|k| !k.starts_with("@@"))
9278                .cloned()
9279                .collect();
9280            for k in keys {
9281                let elem = with_host(|h| match h.get(&val) {
9282                    Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9283                    _ => Value::Undef,
9284                });
9285                let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9286                with_host(|h| {
9287                    if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9288                        if matches!(nv, Value::Undef) {
9289                            p.shift_remove(&k);
9290                        } else {
9291                            p.insert(k.clone(), nv);
9292                        }
9293                    }
9294                });
9295            }
9296        }
9297        _ => {}
9298    }
9299    let kv = with_host(|h| h.new_str(key.to_string()));
9300    // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9301    // primitive, an empty object for an array or an object — node passes it
9302    // either way, and code reading `ctx.source` used to die on `undefined`
9303    // because only two arguments were passed.
9304    let ctx = with_host(|h| {
9305        let mut m: IndexMap<String, Value> = IndexMap::new();
9306        if let Some(s) = source {
9307            let sv = h.new_str(s);
9308            m.insert("source".into(), sv);
9309        }
9310        h.new_object(m)
9311    });
9312    host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9313}
9314
9315struct JsonParser {
9316    chars: Vec<char>,
9317    pos: usize,
9318    /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9319    /// third argument reports as `context.source` (25.5.1.1). Only collected
9320    /// when a reviver was supplied.
9321    ///
9322    /// A flat list rather than a parallel tree because the reviver walk visits
9323    /// primitives in the same depth-first order the parse produced them, so an
9324    /// index into this is enough to line them up.
9325    prims: Vec<String>,
9326    record: bool,
9327}
9328impl JsonParser {
9329    fn peek(&self) -> Option<char> {
9330        self.chars.get(self.pos).copied()
9331    }
9332
9333    /// `at position N (line L column C)` — the location suffix V8 appends to the
9334    /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9335    /// node-js counts `char`s, which agree for the BMP.
9336    fn at(&self, pos: usize) -> String {
9337        let mut line = 1usize;
9338        let mut col = 1usize;
9339        for c in &self.chars[..pos.min(self.chars.len())] {
9340            if *c == '\n' {
9341                line += 1;
9342                col = 1;
9343            } else {
9344                col += 1;
9345            }
9346        }
9347        format!(" at position {pos} (line {line} column {col})")
9348    }
9349
9350    /// A positional error (`Expected ':' after property name in JSON at …`).
9351    fn err_at(&self, what: &str, pos: usize) -> String {
9352        format!("SyntaxError: {what} in JSON{}", self.at(pos))
9353    }
9354
9355    /// The one positional message V8 does NOT suffix with `in JSON`.
9356    fn err_trailing(&self, pos: usize) -> String {
9357        format!(
9358            "SyntaxError: Unexpected non-whitespace character after JSON{}",
9359            self.at(pos)
9360        )
9361    }
9362
9363    /// V8's default parse error: the offending character plus a window of the
9364    /// source. The whole input is quoted when it is short (<= 20 chars);
9365    /// otherwise a 10-character context window either side of `pos` is shown,
9366    /// elided with `...` on whichever side was cut.
9367    fn err_token(&self, pos: usize) -> String {
9368        const MAX_WHOLE: usize = 20;
9369        const CONTEXT: usize = 10;
9370        let len = self.chars.len();
9371        let Some(c) = self.chars.get(pos) else {
9372            return "SyntaxError: Unexpected end of JSON input".into();
9373        };
9374        // V8 reports the whole input for the JS literals that are famously not
9375        // JSON, without naming an offending character.
9376        let whole: String = self.chars.iter().collect();
9377        if matches!(
9378            whole.as_str(),
9379            "undefined" | "NaN" | "Infinity" | "-Infinity"
9380        ) {
9381            return format!("SyntaxError: \"{whole}\" is not valid JSON");
9382        }
9383        let snippet = if len <= MAX_WHOLE {
9384            format!("\"{whole}\"")
9385        } else {
9386            let start = pos.saturating_sub(CONTEXT);
9387            let end = (pos + CONTEXT).min(len);
9388            let body: String = self.chars[start..end].iter().collect();
9389            let head = if start > 0 { "..." } else { "" };
9390            let tail = if end < len { "..." } else { "" };
9391            format!("{head}\"{body}\"{tail}")
9392        };
9393        format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9394    }
9395
9396    fn skip_ws(&mut self) {
9397        while matches!(
9398            self.peek(),
9399            Some(' ') | Some('\n') | Some('\t') | Some('\r')
9400        ) {
9401            self.pos += 1;
9402        }
9403    }
9404    fn parse_value(&mut self) -> Result<Value, String> {
9405        self.skip_ws();
9406        let start = self.pos;
9407        let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9408        let v = match self.peek() {
9409            Some('{') => self.parse_object(),
9410            Some('[') => self.parse_array(),
9411            Some('"') => {
9412                let s = self.parse_string()?;
9413                Ok(with_host(|h| h.new_str(s)))
9414            }
9415            Some('t') | Some('f') => self.parse_bool(),
9416            Some('n') => {
9417                self.expect_lit("null")?;
9418                Ok(with_host(|h| h.null()))
9419            }
9420            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9421            None => Err("SyntaxError: Unexpected end of JSON input".into()),
9422            _ => Err(self.err_token(self.pos)),
9423        }?;
9424        if prim && self.record {
9425            self.prims
9426                .push(self.chars[start..self.pos].iter().collect());
9427        }
9428        Ok(v)
9429    }
9430    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9431        for ch in lit.chars() {
9432            match self.peek() {
9433                Some(c) if c == ch => self.pos += 1,
9434                // V8 reports the first character that broke the literal, which is
9435                // why `foo` complains about `'o'` (index 2) and not `'f'`.
9436                None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9437                _ => return Err(self.err_token(self.pos)),
9438            }
9439        }
9440        Ok(())
9441    }
9442    fn parse_bool(&mut self) -> Result<Value, String> {
9443        if self.peek() == Some('t') {
9444            self.expect_lit("true")?;
9445            Ok(Value::Bool(true))
9446        } else {
9447            self.expect_lit("false")?;
9448            Ok(Value::Bool(false))
9449        }
9450    }
9451    /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9452    /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9453    /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9454    /// it. Each way the grammar can run out has its own message.
9455    fn parse_number(&mut self) -> Result<Value, String> {
9456        let start = self.pos;
9457        if self.peek() == Some('-') {
9458            self.pos += 1;
9459            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9460                return Err(self.err_at("No number after minus sign", self.pos));
9461            }
9462        }
9463        if self.peek() == Some('0') {
9464            self.pos += 1;
9465        } else {
9466            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9467                self.pos += 1;
9468            }
9469        }
9470        if self.peek() == Some('.') {
9471            self.pos += 1;
9472            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9473                return Err(self.err_at("Unterminated fractional number", self.pos));
9474            }
9475            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9476                self.pos += 1;
9477            }
9478        }
9479        if matches!(self.peek(), Some('e') | Some('E')) {
9480            self.pos += 1;
9481            if matches!(self.peek(), Some('+') | Some('-')) {
9482                self.pos += 1;
9483            }
9484            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9485                return Err(self.err_at("Exponent part is missing a number", self.pos));
9486            }
9487            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9488                self.pos += 1;
9489            }
9490        }
9491        let s: String = self.chars[start..self.pos].iter().collect();
9492        s.parse::<f64>()
9493            .map(Value::Float)
9494            .map_err(|_| self.err_at("Unexpected number", start))
9495    }
9496    fn parse_string(&mut self) -> Result<String, String> {
9497        self.pos += 1; // opening quote
9498        let mut out = String::new();
9499        loop {
9500            match self.peek() {
9501                None => return Err(self.err_at("Unterminated string", self.pos)),
9502                Some('"') => {
9503                    self.pos += 1;
9504                    break;
9505                }
9506                Some('\\') => {
9507                    self.pos += 1;
9508                    match self.peek() {
9509                        Some('n') => out.push('\n'),
9510                        Some('t') => out.push('\t'),
9511                        Some('r') => out.push('\r'),
9512                        Some('"') => out.push('"'),
9513                        Some('\\') => out.push('\\'),
9514                        Some('/') => out.push('/'),
9515                        Some('b') => out.push('\u{08}'),
9516                        Some('f') => out.push('\u{0C}'),
9517                        Some('u') => {
9518                            let h: String = self.chars
9519                                [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9520                                .iter()
9521                                .collect();
9522                            if let Ok(n) = u32::from_str_radix(&h, 16) {
9523                                if let Some(ch) = char::from_u32(n) {
9524                                    out.push(ch);
9525                                }
9526                            }
9527                            self.pos += 4;
9528                        }
9529                        _ => {}
9530                    }
9531                    self.pos += 1;
9532                }
9533                // A raw control character is not legal inside a JSON string; it
9534                // has to be escaped. V8 rejects it rather than passing it through.
9535                Some(c) if (c as u32) < 0x20 => {
9536                    return Err(self.err_at("Bad control character in string literal", self.pos))
9537                }
9538                Some(c) => {
9539                    out.push(c);
9540                    self.pos += 1;
9541                }
9542            }
9543        }
9544        Ok(out)
9545    }
9546    fn parse_array(&mut self) -> Result<Value, String> {
9547        self.pos += 1; // [
9548        let mut items = Vec::new();
9549        self.skip_ws();
9550        if self.peek() == Some(']') {
9551            self.pos += 1;
9552            return Ok(with_host(|h| h.new_array(items)));
9553        }
9554        loop {
9555            items.push(self.parse_value()?);
9556            self.skip_ws();
9557            match self.peek() {
9558                Some(',') => {
9559                    self.pos += 1;
9560                }
9561                Some(']') => {
9562                    self.pos += 1;
9563                    break;
9564                }
9565                _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9566            }
9567        }
9568        Ok(with_host(|h| h.new_array(items)))
9569    }
9570    fn parse_object(&mut self) -> Result<Value, String> {
9571        self.pos += 1; // {
9572        let mut props: IndexMap<String, Value> = IndexMap::new();
9573        self.skip_ws();
9574        if self.peek() == Some('}') {
9575            self.pos += 1;
9576            return Ok(with_host(|h| h.new_object(props)));
9577        }
9578        loop {
9579            self.skip_ws();
9580            if self.peek() != Some('"') {
9581                // The first key uses the "or '}'" wording (an empty object is
9582                // still legal there); a key after a comma does not. End of input
9583                // reports the same expectation, at the end position.
9584                return Err(if props.is_empty() {
9585                    self.err_at("Expected property name or '}'", self.pos)
9586                } else {
9587                    self.err_at("Expected double-quoted property name", self.pos)
9588                });
9589            }
9590            let key = self.parse_string()?;
9591            self.skip_ws();
9592            if self.peek() != Some(':') {
9593                return Err(match self.peek() {
9594                    None => "SyntaxError: Unexpected end of JSON input".into(),
9595                    _ => self.err_at("Expected ':' after property name", self.pos),
9596                });
9597            }
9598            self.pos += 1;
9599            let val = self.parse_value()?;
9600            props.insert(key, val);
9601            self.skip_ws();
9602            match self.peek() {
9603                Some(',') => {
9604                    self.pos += 1;
9605                }
9606                Some('}') => {
9607                    self.pos += 1;
9608                    break;
9609                }
9610                _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9611            }
9612        }
9613        Ok(with_host(|h| h.new_object(props)))
9614    }
9615}
9616
9617// ══ type methods (array / string / number) ═══════════════════════════════════
9618
9619fn is_array_method(name: &str) -> bool {
9620    matches!(
9621        name,
9622        "push"
9623            | "pop"
9624            | "shift"
9625            | "unshift"
9626            | "map"
9627            | "filter"
9628            | "forEach"
9629            | "join"
9630            | "slice"
9631            | "indexOf"
9632            | "lastIndexOf"
9633            | "includes"
9634            | "reduce"
9635            | "concat"
9636            | "reverse"
9637            | "sort"
9638            | "find"
9639            | "findIndex"
9640            | "some"
9641            | "every"
9642            | "flat"
9643            | "fill"
9644            | "splice"
9645            | "keys"
9646            | "values"
9647            | "entries"
9648            | "flatMap"
9649            | "at"
9650            | "toString"
9651            | "reduceRight"
9652            | "findLast"
9653            | "findLastIndex"
9654            | "copyWithin"
9655    )
9656}
9657/// Every `String.prototype` method node-js implements.
9658///
9659/// A LIST rather than a `matches!` arm because the same set has to be installed
9660/// on the real `String.prototype` object: a method read off the prototype
9661/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9662/// found nothing there, so the two views of "which methods exist" would drift
9663/// if they were written twice.
9664pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9665    "toUpperCase",
9666    "toLowerCase",
9667    "charAt",
9668    "charCodeAt",
9669    "codePointAt",
9670    "indexOf",
9671    "lastIndexOf",
9672    "includes",
9673    "slice",
9674    "substring",
9675    "substr",
9676    "split",
9677    "trim",
9678    "trimStart",
9679    "trimEnd",
9680    "replace",
9681    "replaceAll",
9682    "repeat",
9683    "startsWith",
9684    "endsWith",
9685    "padStart",
9686    "padEnd",
9687    "concat",
9688    "at",
9689    "toString",
9690    "toLocaleString",
9691    "valueOf",
9692    "match",
9693    "matchAll",
9694    "search",
9695    "normalize",
9696    "localeCompare",
9697    "toLocaleUpperCase",
9698    "toLocaleLowerCase",
9699    "isWellFormed",
9700    "toWellFormed",
9701];
9702
9703fn is_string_method(name: &str) -> bool {
9704    STRING_PROTO_METHODS.contains(&name)
9705}
9706
9707/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9708/// spelled the way this frontend spells the key (`@@iterator`).
9709///
9710/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9711/// .prototype`) installs its methods from a list, and only the string-keyed
9712/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9713/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9714/// `Builtin` namespace and its table gate, answered a function. Derived from
9715/// the table rather than written out, so it cannot name a method node does not
9716/// define nor miss one it does.
9717pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9718    let prefix = format!("@proto:{ctor}:");
9719    crate::arity::BUILTIN_ARITY
9720        .iter()
9721        .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9722        .filter(|m| m.starts_with("@@"))
9723        .collect()
9724}
9725
9726/// The builtin constructors whose `.prototype` object is BRANDED — every other
9727/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9728///
9729/// Measured on node v26.8.1 over every constructor this frontend knows:
9730///
9731/// ```text
9732/// Array/Object/Number/String/Boolean/Function   the ES5 legacy slot prototypes
9733/// Symbol/BigInt/Map/Set/WeakMap/WeakSet         carry an own @@toStringTag
9734/// Promise/Iterator/ArrayBuffer/DataView         "
9735/// WeakRef/FinalizationRegistry/URL              "
9736/// URLSearchParams/TextEncoder/TextDecoder       "
9737/// Date/RegExp/Error/TypeError/Uint8Array/…      [object Object]
9738/// ```
9739///
9740/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9741/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9742/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9743/// its own failure message where node names `[object Object]`.
9744pub(crate) const BRANDED_PROTOS: &[&str] = &[
9745    "Array",
9746    "ArrayBuffer",
9747    "BigInt",
9748    "Boolean",
9749    "DataView",
9750    "FinalizationRegistry",
9751    "Function",
9752    "Iterator",
9753    "Map",
9754    "Number",
9755    "Object",
9756    "Promise",
9757    "Set",
9758    "SharedArrayBuffer",
9759    "String",
9760    "Symbol",
9761    "TextDecoder",
9762    "TextEncoder",
9763    "URL",
9764    "URLSearchParams",
9765    "WeakMap",
9766    "WeakRef",
9767    "WeakSet",
9768];
9769
9770/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9771/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9772/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9773///
9774/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9775/// than coercing `o` to a pattern — the protocol every regexp-like library
9776/// implements. None of the five were consulted, so a custom matcher was
9777/// silently stringified instead.
9778fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9779    if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9780        return None;
9781    }
9782    let f = get_property(arg, sym).ok()?;
9783    with_host(|h| host::is_callable(h, &f)).then_some(f)
9784}
9785
9786fn is_regexp_arg(v: &Value) -> bool {
9787    // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9788    // itself a regexp — or a real one can disown the label. Only the heap kind
9789    // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9790    // the TypeError the spec requires.
9791    if let Ok(m) = get_property(v, "@@match") {
9792        if !matches!(m, Value::Undef) {
9793            return with_host(|h| h.truthy(&m));
9794        }
9795    }
9796    with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9797}
9798
9799/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9800/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9801fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9802    if pat.is_empty() {
9803        return Ok(s.to_string());
9804    }
9805    let mut out = String::new();
9806    let mut rest = s;
9807    let mut base = 0usize;
9808    while let Some(pos) = rest.find(pat) {
9809        out.push_str(&rest[..pos]);
9810        let offset = base + pos;
9811        let m = with_host(|h| h.new_str(pat.to_string()));
9812        let str_arg = with_host(|h| h.new_str(s.to_string()));
9813        let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9814        out.push_str(&with_host(|h| h.str_of(&r)));
9815        let consumed = pos + pat.len();
9816        base += consumed;
9817        rest = &rest[consumed..];
9818        if !all {
9819            break;
9820        }
9821    }
9822    out.push_str(rest);
9823    Ok(out)
9824}
9825/// Every `Number.prototype` method node-js implements — a list for the same
9826/// reason [`STRING_PROTO_METHODS`] is one.
9827pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9828    "toFixed",
9829    "toExponential",
9830    "toString",
9831    "toPrecision",
9832    "toLocaleString",
9833    "valueOf",
9834];
9835
9836fn is_number_method(name: &str) -> bool {
9837    NUMBER_PROTO_METHODS.contains(&name)
9838}
9839
9840/// The exotic kinds whose own dispatch table does NOT already reach the
9841/// `Object.prototype` methods, so the inherited ones have to be routed to.
9842///
9843/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9844/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9845/// it to the object form made `(255).toString(16)` report `[object Number]`.
9846fn inherits_object_methods(recv: &Value) -> bool {
9847    matches!(
9848        with_host(|h| h.kind_of(recv)),
9849        Some(
9850            ObjKind::Map
9851                | ObjKind::Set
9852                | ObjKind::Promise
9853                | ObjKind::RegExp
9854                | ObjKind::Generator
9855                | ObjKind::Symbol
9856                | ObjKind::BigInt
9857                | ObjKind::Iter
9858        )
9859    )
9860}
9861
9862/// Whether `recv`'s own prototype defines `name`, shadowing the
9863/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9864/// `Map.prototype` does not.
9865fn overrides_object_method(recv: &Value, name: &str) -> bool {
9866    match with_host(|h| h.kind_of(recv)) {
9867        Some(ObjKind::Map) => is_map_method(name),
9868        Some(ObjKind::Set) => is_set_method(name),
9869        Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9870        // A Symbol has its own `toString`; `valueOf` is the inherited one,
9871        // which returns the receiver — exactly what a symbol needs.
9872        Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9873        Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9874        _ => false,
9875    }
9876}
9877
9878/// Dispatch `recv.name(args)` for the built-in prototype methods.
9879pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9880    // A USER method on the receiver's prototype chain wins over the builtin of
9881    // the same name — that is how a `class X extends Array` method is reached,
9882    // since the dispatch below goes straight to the builtin table and has no
9883    // entry for it.
9884    //
9885    // Deliberately restricted to a user function: the shared `Object.prototype`
9886    // carries real `@proto:Object:*` thunks, so accepting any callable made a
9887    // bare `map.toString()` resolve to the object form instead of the builtin
9888    // one the exotic is supposed to use.
9889    if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9890        if matches!(
9891            with_host(|h| h.kind_of(&f)),
9892            Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9893        ) {
9894            return host::invoke(&f, args, Some(recv.clone()));
9895        }
9896    }
9897    // A method synthesized from the receiver's KIND is unreachable once its
9898    // intrinsic prototype is off the chain. The read already answers
9899    // `undefined` for one; dispatch has its own table and would still have
9900    // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9901    // while `a.join` was `undefined` — the read and the call disagreeing again,
9902    // in the opposite direction from the monkey-patch case below.
9903    if !own_intrinsic_reachable(recv)
9904        && inherited_method_owner(recv, name).is_none()
9905        && !has_own_for_shadow(recv, name)
9906        && inherited_builtin_static(recv, name).is_none()
9907        && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9908    {
9909        return Err(host::type_error(&format!("{name} is not a function")));
9910    }
9911    // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9912    // path resolves these, but dispatch goes straight to the builtin table and
9913    // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9914    // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9915    // about the same name, on the one path a polyfill actually uses.
9916    if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9917        if let Some(f) = inherited_builtin_static(recv, name) {
9918            if with_host(|h| host::is_callable(h, &f)) {
9919                return host::invoke(&f, args, Some(recv.clone()));
9920            }
9921        }
9922    }
9923    // Every object INHERITS the `Object.prototype` methods, and an exotic that
9924    // does not define its own reaches them the same way. Each kind's dispatch
9925    // table below only knows its own methods, so `new Map().toString()`,
9926    // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9927    // not a function" — `Object.prototype.toString.call(m)` worked while
9928    // `m.toString()` did not.
9929    // The allowlist is the kinds whose own dispatch table below would otherwise
9930    // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9931    // the same way — a function, a class and a bound function included, where
9932    // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9933    // resolved it. `inherited_method_owner` decides which prototype owns the
9934    // name, so a kind that defines its own still gets its own.
9935    if is_object_builtin_method(name)
9936        && (inherited_method_owner(recv, name) == Some("Object")
9937            || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9938    {
9939        // `toString` goes through the branded form (20.1.3.6), which reads
9940        // `Symbol.toStringTag` and falls back to the receiver's own brand —
9941        // `[object Map]`, not the generic stringification.
9942        if name == "toString" {
9943            return proto_method(recv, "Object:toString", args);
9944        }
9945        return object_builtin_method(recv, name, args);
9946    }
9947    // `Object.prototype.valueOf` is inherited by every exotic that does not
9948    // override it (an Array does not), and returns the receiver. Without this
9949    // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9950    // and threw `valueOf is not a function`.
9951    if name == "valueOf"
9952        && matches!(
9953            with_host(|h| h.kind_of(recv)),
9954            Some(
9955                ObjKind::Array
9956                    | ObjKind::Map
9957                    | ObjKind::Set
9958                    | ObjKind::Generator
9959                    | ObjKind::Promise
9960                    | ObjKind::Iter
9961                    | ObjKind::RegExp
9962            )
9963        )
9964    {
9965        return Ok(recv.clone());
9966    }
9967    // Only the tag is needed to pick the branch — cloning the receiver here made
9968    // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9969    match with_host(|h| h.kind_of(recv)) {
9970        Some(ObjKind::Array) => array_method(recv, name, args),
9971        Some(ObjKind::Str) => {
9972            // `string_method` consumes the text itself, so this clone is the
9973            // payload, not a tag probe.
9974            let s = peek(recv, |o| match o {
9975                JsObj::Str(s) => Some(s.clone()),
9976                _ => None,
9977            })
9978            .unwrap_or_default();
9979            string_method(&s, name, args)
9980        }
9981        Some(ObjKind::Map) => map_method(recv, name, args),
9982        Some(ObjKind::Set) => set_method(recv, name, args),
9983        Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9984            crate::stdlib::iterator::call(recv, name, &args)
9985        }
9986        Some(ObjKind::Generator) => generator_method(recv, name, args),
9987        Some(ObjKind::Promise) => promise_method(recv, name, args),
9988        Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9989            crate::stdlib::iterator::call(recv, name, &args)
9990        }
9991        Some(ObjKind::Iter) => iter_method(recv, name, args),
9992        Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9993        Some(ObjKind::BigInt) => {
9994            let b = peek(recv, |o| match o {
9995                JsObj::BigInt(b) => Some(b.clone()),
9996                _ => None,
9997            })
9998            .unwrap_or_default();
9999            bigint_method(&b, name, args)
10000        }
10001        Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
10002        Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
10003            match function_builtin_method(recv, name, &args)? {
10004                Some(v) => Ok(v),
10005                None => Err(host::type_error(&format!("{name} is not a function"))),
10006            }
10007        }
10008        Some(ObjKind::Object) => {
10009            if let Some(f) = peek(recv, |o| match o {
10010                JsObj::Object(p) => p.get(name).cloned(),
10011                _ => None,
10012            }) {
10013                host::invoke(&f, args, Some(recv.clone()))
10014            } else if name == "hasOwnProperty" {
10015                let k = with_host(|h| h.str_of(&arg0(&args)));
10016                let has = peek(recv, |o| match o {
10017                    JsObj::Object(p) => Some(p.contains_key(&k)),
10018                    _ => None,
10019                })
10020                .unwrap_or(false);
10021                Ok(Value::Bool(has))
10022            } else if name == "toString" {
10023                Ok(with_host(|h| h.new_str("[object Object]")))
10024            } else {
10025                Err(host::type_error(&format!("{} is not a function", name)))
10026            }
10027        }
10028        _ => {
10029            // Primitive number/bool/string coercions.
10030            if let Value::Float(_) | Value::Int(_) = recv {
10031                return number_method(with_host(|h| h.to_number(recv)), name, args);
10032            }
10033            if let Some(s) = with_host(|h| h.as_str(recv)) {
10034                return string_method(&s, name, args);
10035            }
10036            // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
10037            // so it reached no branch at all and `true.toString()` threw `is not
10038            // a function`. Its three methods are `toString`, `valueOf`, and the
10039            // inherited `Object.prototype.toLocaleString` — which
10040            // `[1,'a',true].toLocaleString()` invokes per element, so the hole
10041            // was reachable from the array form too.
10042            if let Value::Bool(b) = recv {
10043                return match name {
10044                    "toString" | "toLocaleString" => {
10045                        Ok(new_s(if *b { "true" } else { "false" }.to_string()))
10046                    }
10047                    "valueOf" => Ok(Value::Bool(*b)),
10048                    _ => Err(host::type_error(&format!("{name} is not a function"))),
10049                };
10050            }
10051            Err(host::type_error(&format!("{} is not a function", name)))
10052        }
10053    }
10054}
10055
10056/// A copy of the whole backing store, for the methods that genuinely consume
10057/// every element (`map`, `filter`, `join`, …). Never call it just to read
10058/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10059/// A LIVE iterator over a `Map` or `Set`.
10060///
10061/// Node's collection iterators see the collection as it is at each step: an
10062/// entry added during iteration IS visited, and one deleted before it is
10063/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10064/// a loop that deletes as it goes still processed the entries it had removed.
10065///
10066/// The cursor is the last key yielded plus the index it was at. On each step
10067/// the key is located again in the CURRENT order: if it is still there the next
10068/// entry follows it, and if it was itself deleted the stored index now names
10069/// the entry that shifted into its place. That reproduces node for the cases
10070/// its own tests turn on — add-during, delete-ahead, delete-self,
10071/// delete-behind, delete-the-rest and clear — without giving `Map` the
10072/// tombstoned entry list node uses internally.
10073fn collection_iterator(coll: &Value, kind: &str) -> Value {
10074    with_host(|h| {
10075        let mut m = IndexMap::new();
10076        m.insert(
10077            "@@native".into(),
10078            h.new_str("CollectionIterator".to_string()),
10079        );
10080        m.insert("@@coll".into(), coll.clone());
10081        m.insert("@@kind".into(), h.new_str(kind.to_string()));
10082        m.insert("@@started".into(), Value::Bool(false));
10083        m.insert("@@lastIdx".into(), Value::Float(0.0));
10084        h.new_object(m)
10085    })
10086}
10087
10088/// One step of a live collection iterator.
10089pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10090    let slot = |k: &str| {
10091        with_host(|h| match h.get(recv) {
10092            Some(JsObj::Object(p)) => p.get(k).cloned(),
10093            _ => None,
10094        })
10095    };
10096    let coll = slot("@@coll").unwrap_or(Value::Undef);
10097    let kind = slot("@@kind")
10098        .map(|v| with_host(|h| h.str_of(&v)))
10099        .unwrap_or_default();
10100    let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10101    let last_idx = slot("@@lastIdx")
10102        .map(|v| with_host(|h| h.to_number(&v)) as usize)
10103        .unwrap_or(0);
10104    let last_key = slot("@@lastKey");
10105
10106    let next_idx = if !started {
10107        0
10108    } else {
10109        match last_key
10110            .as_ref()
10111            .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10112        {
10113            // Still present: continue after it.
10114            Some(i) => i + 1,
10115            // Deleted since: whatever shifted into its slot is next.
10116            None => last_idx,
10117        }
10118    };
10119    let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10120    let Some((k, v)) = entry else {
10121        return Ok(iter_result(Value::Undef, true));
10122    };
10123    with_host(|h| {
10124        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10125            p.insert("@@started".into(), Value::Bool(true));
10126            p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10127            p.insert("@@lastKey".into(), k.clone());
10128        }
10129    });
10130    let out = match kind.as_str() {
10131        "keys" => k,
10132        "values" => v,
10133        _ => with_host(|h| h.new_array(vec![k, v])),
10134    };
10135    Ok(iter_result(out, false))
10136}
10137
10138/// What `util.inspect` shows for a live `Map`/`Set` iterator: its brand
10139/// (`Map Iterator`, `Map Entries`, `Set Iterator`, `Set Entries`) and the
10140/// entries it has still to yield, as `(key, value)` pairs — without advancing
10141/// it. `None` when `v` is not one.
10142pub(crate) fn collection_iterator_view(
10143    h: &host::JsHost,
10144    v: &Value,
10145) -> Option<(&'static str, Vec<(Value, Value)>)> {
10146    let Some(JsObj::Object(p)) = h.get(v) else {
10147        return None;
10148    };
10149    if p.get("@@native").map(|t| h.str_of(t)).as_deref() != Some("CollectionIterator") {
10150        return None;
10151    }
10152    let coll = p.get("@@coll").cloned().unwrap_or(Value::Undef);
10153    let kind = p.get("@@kind").map(|k| h.str_of(k)).unwrap_or_default();
10154    let started = p.get("@@started").is_some_and(|s| h.truthy(s));
10155    let last_idx = p
10156        .get("@@lastIdx")
10157        .map(|n| h.to_number(n) as usize)
10158        .unwrap_or(0);
10159    let is_map = matches!(h.get(&coll), Some(JsObj::Map { .. }));
10160    let brand = match (is_map, kind == "entries") {
10161        (true, true) => "Map Entries",
10162        (true, false) => "Map Iterator",
10163        (false, true) => "Set Entries",
10164        (false, false) => "Set Iterator",
10165    };
10166    // The same cursor rule `collection_iterator_next` steps by.
10167    let mut idx = if !started {
10168        0
10169    } else {
10170        match p
10171            .get("@@lastKey")
10172            .and_then(|k| collection_index_of(h, &coll, k))
10173        {
10174            Some(i) => i + 1,
10175            None => last_idx,
10176        }
10177    };
10178    let mut rest = Vec::new();
10179    while let Some((k, val)) = collection_entry_at(h, &coll, idx) {
10180        rest.push(match kind.as_str() {
10181            "keys" => (k.clone(), k),
10182            "values" => (val.clone(), val),
10183            _ => (k, val),
10184        });
10185        idx += 1;
10186    }
10187    Some((brand, rest))
10188}
10189
10190/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10191fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10192    match h.get(coll) {
10193        Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10194        Some(JsObj::Set { entries, .. }) => {
10195            entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10196        }
10197        _ => None,
10198    }
10199}
10200
10201/// Where `key` currently sits in the collection's order.
10202fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10203    let mk = host::map_key(h, key);
10204    match h.get(coll) {
10205        Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10206        Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10207        _ => None,
10208    }
10209}
10210
10211/// The `thisArg` an iteration method was given, if any.
10212///
10213/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10214/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10215/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10216/// `Array.from`'s map function. Every one of them was invoking the callback
10217/// with no receiver, so `this` inside it was undefined and the argument did
10218/// nothing.
10219fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10220    args.get(idx)
10221        .filter(|v| !matches!(v, Value::Undef))
10222        .cloned()
10223}
10224
10225/// The elements of an array, with any INDEX ACCESSOR resolved.
10226///
10227/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10228/// table, and an array's elements live in a backing vector — so every method
10229/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10230/// slot and never called the getter, while a plain `arr[1]` read did.
10231///
10232/// An array with no accessors pays one lookup returning an empty list, so the
10233/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10234/// since calling one re-enters.
10235/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10236/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10237/// read LIVE at its index, and an index that no longer exists is skipped.
10238///
10239/// Snapshotting the whole array instead meant a callback that mutated it was
10240/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10241/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10242///
10243/// `f` returns `Some(x)` to stop early with `x`.
10244fn array_walk<T>(
10245    recv: &Value,
10246    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10247) -> Result<Option<T>, String> {
10248    let len = array_len(recv);
10249    for i in 0..len {
10250        // A HOLE — and an index a shrinking mutation has dropped — is skipped
10251        // without calling the callback.
10252        if index_absent(recv, i) || i >= array_len(recv) {
10253            continue;
10254        }
10255        let v = get_property(recv, &i.to_string())?;
10256        if let Some(out) = f(i, v)? {
10257            return Ok(Some(out));
10258        }
10259    }
10260    Ok(None)
10261}
10262
10263/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10264/// capture-length-once, read-each-element-live rule walked from the end. A
10265/// callback that SHRINKS the array is observed by every later step, so the
10266/// indices it drops are skipped rather than served from a stale copy.
10267fn array_walk_rev<T>(
10268    recv: &Value,
10269    from: usize,
10270    mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10271) -> Result<Option<T>, String> {
10272    for i in (0..from).rev() {
10273        if index_absent(recv, i) || i >= array_len(recv) {
10274            continue;
10275        }
10276        let v = get_property(recv, &i.to_string())?;
10277        if let Some(out) = f(i, v)? {
10278            return Ok(Some(out));
10279        }
10280    }
10281    Ok(None)
10282}
10283
10284/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10285/// `undefined` once a mutation has shrunk the array past it.
10286fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10287    if i >= array_len(recv) {
10288        return Ok(Value::Undef);
10289    }
10290    get_property(recv, &i.to_string())
10291}
10292
10293fn array_items(recv: &Value) -> Vec<Value> {
10294    let mut items = with_host(|h| match h.get(recv) {
10295        Some(JsObj::Array(items)) => items.clone(),
10296        _ => Vec::new(),
10297    });
10298    resolve_index_accessors(recv, &mut items);
10299    items
10300}
10301
10302/// Replace each slot that has an own accessor with what its getter returns.
10303pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10304    resolve_index_accessors(recv, items);
10305}
10306
10307/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10308/// the ORIGINAL array when nothing changed, and the original still holds the
10309/// stale slots.
10310fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10311    let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10312        .into_iter()
10313        .filter_map(|k| k.parse::<usize>().ok())
10314        .filter(|i| *i < items.len())
10315        .collect();
10316    // An ELIDED index the prototype chain supplies is stale in the backing
10317    // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10318    // value. Spread and `JSON.stringify` both read through here, and both
10319    // rendered the hole rather than what `a[i]` reads.
10320    let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10321        .into_iter()
10322        .filter(|i| *i < items.len() && !indices.contains(i))
10323        .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10324        .collect();
10325    indices.extend(inherited);
10326    let mut replaced = false;
10327    for i in indices {
10328        if let Ok(v) = get_property(recv, &i.to_string()) {
10329            items[i] = v;
10330            replaced = true;
10331        }
10332    }
10333    replaced
10334}
10335
10336/// The ELIDED positions of array `recv` as a membership set. A dense array —
10337/// which is nearly every array — answers with an empty set after a single
10338/// negative hash probe and allocates nothing.
10339///
10340/// The iteration methods split into two groups, and the split is not a matter of
10341/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10342/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10343/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10344/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10345fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10346    with_host(|h| h.hole_indices(recv)).into_iter().collect()
10347}
10348
10349/// The indices `recv` genuinely has NO property at — the elided ones the
10350/// prototype chain does not supply either.
10351///
10352/// Every array method tests `HasProperty` before deciding to skip a position
10353/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10354/// alone made an inherited element invisible to all of them: with
10355/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10356/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10357/// the same position.
10358///
10359/// `hole_set` remains the elision record itself, which is what `splice` moves
10360/// around — that bookkeeping is about the array's OWN storage and must not
10361/// consult the chain.
10362fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10363    hole_set(recv)
10364        .into_iter()
10365        .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10366        .collect()
10367}
10368
10369/// The single-index form of [`absent_set`], for the walkers that test one
10370/// position at a time.
10371fn index_absent(recv: &Value, i: usize) -> bool {
10372    with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10373}
10374
10375/// The element count, without copying the elements.
10376fn array_len(recv: &Value) -> usize {
10377    peek(recv, |o| match o {
10378        JsObj::Array(items) => Some(items.len()),
10379        _ => None,
10380    })
10381    .unwrap_or(0)
10382}
10383
10384/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10385/// array method builds its RESULT with.
10386///
10387/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10388/// produce an array of the receiver's own species, so on a `class A extends
10389/// Array` the result is an `A`. Every one of them allocated a plain array
10390/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10391///
10392/// The default `get [Symbol.species]() { return this }` is what makes the
10393/// subclass the species; a class overriding it with `Array` gets a plain array
10394/// back, which is the documented way to opt out.
10395/// Build an array-shaped result through `ctor`, or a plain array when there is
10396/// none to build through.
10397///
10398/// The constructor is called with the LENGTH and the elements written after, as
10399/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10400/// constructor observe the allocation.
10401fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10402    let Some(ctor) = ctor.filter(|c| {
10403        matches!(
10404            with_host(|h| h.kind_of(c)),
10405            Some(ObjKind::Class) | Some(ObjKind::Func)
10406        )
10407    }) else {
10408        return Ok(with_host(|h| h.new_array(items)));
10409    };
10410    let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10411    write_elements(&out, items);
10412    Ok(out)
10413}
10414
10415/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10416/// hole marks the length-only construction left behind.
10417///
10418/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10419/// the elements written over them stayed marked — so every subclass result of
10420/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10421/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10422fn write_elements(out: &Value, items: Vec<Value>) {
10423    with_host(|h| {
10424        h.clear_holes(out);
10425        if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10426            *dst = items;
10427        }
10428    });
10429}
10430
10431fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10432    let plain = || with_host(|h| h.new_array(items.clone()));
10433    // Only a subclass instance can have a species of its own: a plain array's
10434    // `constructor` is the `Array` builtin, whose species is `Array`.
10435    // A chain lookup, not `get_property`: an Array receiver resolves its
10436    // properties through the stdlib funnel, which has no `constructor` entry,
10437    // so the read alone reports `undefined` for every subclass instance. A
10438    // Proxy is the exception — it has no property map to walk, and its
10439    // `constructor` comes from the `get` trap, so a proxied subclass array
10440    // produced plain arrays.
10441    let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10442        get_property(recv, "constructor").unwrap_or(Value::Undef)
10443    } else {
10444        with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10445    };
10446    if !matches!(
10447        with_host(|h| h.kind_of(&ctor)),
10448        Some(ObjKind::Class) | Some(ObjKind::Func)
10449    ) {
10450        return Ok(plain());
10451    }
10452    // `C[@@species]` (23.1.3.4 step 5): a subclass that does not override the
10453    // accessor reads back ITSELF (the `@@species` arm of the class static
10454    // lookup), so an `undefined` or `null` here was written by user code — a
10455    // `static get [Symbol.species]() { return undefined }` — and both mean
10456    // "make a plain Array". A getter that throws propagates.
10457    let species = match get_property(&ctor, "@@species")? {
10458        Value::Undef => return Ok(plain()),
10459        s if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10460        s => s,
10461    };
10462    if !matches!(
10463        with_host(|h| h.kind_of(&species)),
10464        Some(ObjKind::Class) | Some(ObjKind::Func)
10465    ) {
10466        return Ok(plain());
10467    }
10468    let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10469    // The constructor is called with the LENGTH, so the elements are written
10470    // afterwards — which is also what lets a subclass constructor observe the
10471    // allocation, as node's does.
10472    write_elements(&out, items);
10473    Ok(out)
10474}
10475
10476fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10477    array_method_on(recv, recv, name, args)
10478}
10479
10480/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10481/// generic path to copy the result back onto the array-like.
10482const ARRAY_MUTATORS: &[&str] = &[
10483    "push",
10484    "pop",
10485    "shift",
10486    "unshift",
10487    "splice",
10488    "sort",
10489    "reverse",
10490    "fill",
10491    "copyWithin",
10492];
10493
10494/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10495/// a DOM-ish collection, `arguments`).
10496///
10497/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10498/// rather than over an Array's element vector, so the receiver only has to have
10499/// a `length`. The elements are read out into a temporary Array, the ordinary
10500/// implementation runs on that, and a MUTATING method writes the result back —
10501/// which keeps one implementation of each method rather than a second, generic
10502/// one that could drift from it.
10503///
10504/// An index the receiver does not own is a HOLE in the temporary, so the
10505/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10506fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10507    let len = match get_property(recv, "length") {
10508        Ok(v) => host::to_array_length(&v).unwrap_or(0),
10509        Err(_) => 0,
10510    };
10511    // A STRING receiver owns every index of its length; `has_property` answers
10512    // for objects and reports none of them, which made `[].map.call('abc', f)`
10513    // an array of three holes.
10514    let dense = with_host(|h| h.as_str(recv)).is_some();
10515    let mut items = Vec::with_capacity(len);
10516    let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10517    for i in 0..len {
10518        let k = i.to_string();
10519        if dense || has_property(recv, &k)? {
10520            items.push(get_property(recv, &k)?);
10521        } else {
10522            holes.insert(i);
10523            items.push(Value::Undef);
10524        }
10525    }
10526    let tmp = with_host(|h| {
10527        let a = h.new_array(items);
10528        h.install_holes(&a, holes);
10529        a
10530    });
10531    let out = array_method_on(&tmp, recv, method, args)?;
10532    if ARRAY_MUTATORS.contains(&method) {
10533        let result = with_host(|h| match h.get(&tmp) {
10534            Some(JsObj::Array(items)) => items.clone(),
10535            _ => Vec::new(),
10536        });
10537        for (i, v) in result.iter().enumerate() {
10538            set_property(recv, &i.to_string(), v.clone())?;
10539        }
10540        set_property(recv, "length", Value::Float(result.len() as f64))?;
10541    }
10542    Ok(out)
10543}
10544
10545/// `Array.prototype.<name>` on `recv`.
10546///
10547/// `this_value` is what a callback receives as its third argument and what a
10548/// mutating method returns — the same object as `recv` for an ordinary array
10549/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10550/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10551fn array_method_on(
10552    recv: &Value,
10553    this_value: &Value,
10554    name: &str,
10555    args: Vec<Value>,
10556) -> Result<Value, String> {
10557    let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10558    match name {
10559        "push" => {
10560            // 23.1.3.23 step 4 defines each new element through
10561            // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10562            // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10563            // TypeError. The elements were appended to the backing vector
10564            // regardless, so sealing an array did not seal it.
10565            if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10566                let at = array_len(recv);
10567                return Err(host::type_error(&format!(
10568                    "Cannot add property {at}, object is not extensible"
10569                )));
10570            }
10571            // Step 5 then SETS `length`, so a non-writable one refuses the push
10572            // too — `defineProperty(a, 'length', {writable: false})` makes an
10573            // array append-proof without sealing it.
10574            if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10575                return Err(host::type_error(
10576                    "Cannot assign to read only property 'length' of object '[object Array]'",
10577                ));
10578            }
10579            // `push` returns the new length; take it from the same mutable
10580            // borrow rather than copying the array back out to count it.
10581            let len = with_host(|h| {
10582                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10583                    items.extend(args.iter().cloned());
10584                    items.len()
10585                } else {
10586                    0
10587                }
10588            });
10589            Ok(Value::Float(len as f64))
10590        }
10591        "pop" => Ok(with_host(|h| {
10592            let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10593                items.pop().unwrap_or(Value::Undef)
10594            } else {
10595                Value::Undef
10596            };
10597            let len = match h.get(recv) {
10598                Some(JsObj::Array(items)) => items.len(),
10599                _ => 0,
10600            };
10601            h.truncate_holes(recv, len);
10602            popped
10603        })),
10604        "shift" => Ok(with_host(|h| {
10605            let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10606                if items.is_empty() {
10607                    Value::Undef
10608                } else {
10609                    items.remove(0)
10610                }
10611            } else {
10612                Value::Undef
10613            };
10614            h.remap_holes(recv, |i| i.checked_sub(1));
10615            shifted
10616        })),
10617        "unshift" => {
10618            with_host(|h| {
10619                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10620                    for (i, a) in args.iter().enumerate() {
10621                        items.insert(i, a.clone());
10622                    }
10623                }
10624                let n = args.len();
10625                h.remap_holes(recv, |i| Some(i + n));
10626            });
10627            Ok(Value::Float(array_len(recv) as f64))
10628        }
10629        "join" => {
10630            let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10631                ",".to_string()
10632            } else {
10633                arg_to_string(&args, 0)?
10634            };
10635            join_array(recv, &sep)
10636        }
10637        // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10638        // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10639        // empty string. It threw `is not a function` — the whole method was
10640        // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10641        "toLocaleString" => {
10642            // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10643            // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10644            if !host::join_stack_push(recv) {
10645                return Ok(with_host(|h| h.new_str(String::new())));
10646            }
10647            let items = array_items(recv);
10648            let mut parts: Vec<String> = Vec::with_capacity(items.len());
10649            for it in &items {
10650                if with_host(|h| h.is_nullish(it)) {
10651                    parts.push(String::new());
10652                    continue;
10653                }
10654                let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10655                    Ok(v) => v,
10656                    Err(e) => {
10657                        host::join_stack_pop();
10658                        return Err(e);
10659                    }
10660                };
10661                parts.push(with_host(|h| h.str_of(&v)));
10662            }
10663            host::join_stack_pop();
10664            Ok(with_host(|h| h.new_str(parts.join(","))))
10665        }
10666        // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10667        // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10668        // `Get`-based `includes` reports `true` for the same array.
10669        "indexOf" => {
10670            let target = arg0(&args);
10671            let len = array_len(recv);
10672            let start = search_start(arg_num(&args, 1), len);
10673            let mut idx = None;
10674            for i in start..len {
10675                // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10676                // index a mutation has since dropped — is skipped, not compared.
10677                if index_absent(recv, i) || i >= array_len(recv) {
10678                    continue;
10679                }
10680                let x = get_property(recv, &i.to_string())?;
10681                if with_host(|h| h.strict_eq(&x, &target)) {
10682                    idx = Some(i);
10683                    break;
10684                }
10685            }
10686            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10687        }
10688        "lastIndexOf" => {
10689            let items = array_items(recv);
10690            let holes = absent_set(recv);
10691            let target = arg0(&args);
10692            let from = (args.len() > 1).then(|| arg_num(&args, 1));
10693            let idx = match search_start_last(from, items.len()) {
10694                None => None,
10695                Some(start) => with_host(|h| {
10696                    items[..=start]
10697                        .iter()
10698                        .enumerate()
10699                        .rev()
10700                        .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10701                        .map(|(i, _)| i)
10702                }),
10703            };
10704            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10705        }
10706        "includes" => {
10707            // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10708            // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10709            // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10710            let target = arg0(&args);
10711            let tnan = matches!(target, Value::Float(f) if f.is_nan());
10712            let len = array_len(recv);
10713            let start = search_start(arg_num(&args, 1), len);
10714            let mut found = false;
10715            for i in start..len {
10716                let x = array_elem_live(recv, i)?;
10717                if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10718                    || with_host(|h| h.strict_eq(&x, &target))
10719                {
10720                    found = true;
10721                    break;
10722                }
10723            }
10724            Ok(Value::Bool(found))
10725        }
10726        "slice" => {
10727            let items = array_items(recv);
10728            let (lo, hi) = slice_bounds(&args, items.len());
10729            let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10730            with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10731            Ok(out)
10732        }
10733        "concat" => {
10734            // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10735            // is spread, overriding `IsArray` in BOTH directions: a plain
10736            // array-like opts IN, and an array opts OUT.
10737            let spreadable = |a: &Value| -> bool {
10738                let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10739                if matches!(flag, Value::Undef) {
10740                    matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10741                        && !is_arguments(a)
10742                } else {
10743                    with_host(|h| h.truthy(&flag))
10744                }
10745            };
10746            // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10747            // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10748            // element, its `ToObject` box, not the characters `array_generic`
10749            // read out of it. A hole in a spread receiver or argument stays a
10750            // hole in the result, at its shifted position.
10751            let (mut out, mut holes) = if spreadable(this_value) {
10752                (array_items(recv), absent_set(recv))
10753            } else {
10754                (vec![to_object(this_value)], Default::default())
10755            };
10756            let mut sources: Vec<(Value, usize)> = Vec::new();
10757            for a in &args {
10758                if !spreadable(a) {
10759                    out.push(a.clone());
10760                    continue;
10761                }
10762                match with_host(|h| h.get(a).cloned()) {
10763                    // Read off the backing vector rather than through
10764                    // `array_items`, so the resolve that does for the receiver
10765                    // has to be done here too: 23.1.3.1 step 5.c.iv is a
10766                    // `[[Get]]`, and an index with a getter — or an elided one
10767                    // the chain supplies — is stale in that vector.
10768                    Some(JsObj::Array(mut items)) => {
10769                        resolve_index_accessors(a, &mut items);
10770                        sources.push((a.clone(), out.len()));
10771                        out.extend(items);
10772                    }
10773                    // An opted-in array-LIKE spreads by its `length` and index
10774                    // properties rather than by a backing vector it has none of.
10775                    _ => {
10776                        let len = get_property(a, "length").unwrap_or(Value::Undef);
10777                        let n = with_host(|h| h.to_number(&len));
10778                        let n = if n.is_finite() {
10779                            n.max(0.0) as usize
10780                        } else {
10781                            0
10782                        };
10783                        for i in 0..n {
10784                            out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10785                        }
10786                    }
10787                }
10788            }
10789
10790            for (src, base) in sources {
10791                holes.extend(
10792                    with_host(|h| h.hole_indices(&src))
10793                        .into_iter()
10794                        .map(|i| i + base),
10795                );
10796            }
10797            let arr = array_species_create(this_value, out)?;
10798            with_host(|h| h.install_holes(&arr, holes));
10799            Ok(arr)
10800        }
10801        "reverse" => {
10802            let len = array_len(recv);
10803            with_host(|h| {
10804                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10805                    items.reverse();
10806                }
10807                h.remap_holes(recv, |i| Some(len - 1 - i));
10808            });
10809            Ok(this_value.clone())
10810        }
10811        "fill" => {
10812            // fill(value[, start[, end]]) — negative indices count from the end.
10813            let val = arg0(&args);
10814            let len = array_len(recv) as i64;
10815            let norm =
10816                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10817            let start = if args.len() >= 2 {
10818                norm(arg_num(&args, 1) as i64)
10819            } else {
10820                0
10821            };
10822            let end = if args.len() >= 3 {
10823                norm(arg_num(&args, 2) as i64)
10824            } else {
10825                len as usize
10826            };
10827            with_host(|h| {
10828                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10829                    for it in items.iter_mut().take(end).skip(start) {
10830                        *it = val.clone();
10831                    }
10832                }
10833                // Every filled position now holds a real value.
10834                h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10835            });
10836            Ok(this_value.clone())
10837        }
10838        "copyWithin" => {
10839            // copyWithin(target, start[, end]) — copy a slice within the array.
10840            let items = array_items(recv);
10841            let len = items.len() as i64;
10842            let norm =
10843                |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10844            let target = norm(arg_num(&args, 0) as i64);
10845            let start = if args.len() >= 2 {
10846                norm(arg_num(&args, 1) as i64)
10847            } else {
10848                0
10849            };
10850            let end = if args.len() >= 3 {
10851                norm(arg_num(&args, 2) as i64)
10852            } else {
10853                len as usize
10854            };
10855            let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10856            let copied = slice.len();
10857            // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10858            // deletes the target when the source has no such property);
10859            // everything outside the written range keeps its own.
10860            let src_holes = absent_set(recv);
10861            with_host(|h| {
10862                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10863                    for (k, v) in slice.into_iter().enumerate() {
10864                        if target + k < a.len() {
10865                            a[target + k] = v;
10866                        }
10867                    }
10868                }
10869                let len = len as usize;
10870                let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10871                    .iter()
10872                    .copied()
10873                    .filter(|i| *i < target || *i >= (target + copied).min(len))
10874                    .collect();
10875                for k in 0..copied {
10876                    if target + k < len && src_holes.contains(&(start + k)) {
10877                        holes.insert(target + k);
10878                    }
10879                }
10880                h.install_holes(recv, holes);
10881            });
10882            Ok(this_value.clone())
10883        }
10884        "at" => {
10885            let items = array_items(recv);
10886            let mut i = arg_num(&args, 0) as i64;
10887            if i < 0 {
10888                i += items.len() as i64;
10889            }
10890            Ok(if i >= 0 && (i as usize) < items.len() {
10891                items[i as usize].clone()
10892            } else {
10893                Value::Undef
10894            })
10895        }
10896        // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10897        // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10898        // twice and yields `[2, <1 empty item>, 6]`.
10899        "map" => {
10900            let holes = absent_set(recv);
10901            let cb = arg0(&args);
10902            // The result keeps the source's LENGTH, so a skipped index still
10903            // occupies a slot; `array_walk` only tells us which ones ran.
10904            let mut out = vec![Value::Undef; array_len(recv)];
10905            array_walk(recv, |i, it| {
10906                let v = host::invoke(
10907                    &cb,
10908                    vec![it, Value::Float(i as f64), this_value.clone()],
10909                    this_arg(&args, 1),
10910                )?;
10911                if i < out.len() {
10912                    out[i] = v;
10913                }
10914                Ok(None::<()>)
10915            })?;
10916            let arr = array_species_create(this_value, out)?;
10917            with_host(|h| h.install_holes(&arr, holes));
10918            Ok(arr)
10919        }
10920        "flatMap" => {
10921            let cb = arg0(&args);
10922            let thisarg = this_arg(&args, 1);
10923            let mut out = Vec::new();
10924            array_walk(recv, |i, v| {
10925                let r = host::invoke(
10926                    &cb,
10927                    vec![v, Value::Float(i as f64), this_value.clone()],
10928                    thisarg.clone(),
10929                )?;
10930                match with_host(|h| h.get(&r).cloned()) {
10931                    Some(JsObj::Array(inner)) => out.extend(inner),
10932                    _ => out.push(r),
10933                }
10934                Ok(None::<()>)
10935            })?;
10936            array_species_create(this_value, out)
10937        }
10938        "filter" => {
10939            let cb = arg0(&args);
10940            let mut out = Vec::new();
10941            array_walk(recv, |i, it| {
10942                let keep = host::invoke(
10943                    &cb,
10944                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10945                    this_arg(&args, 1),
10946                )?;
10947                if with_host(|h| h.truthy(&keep)) {
10948                    out.push(it);
10949                }
10950                Ok(None::<()>)
10951            })?;
10952            array_species_create(this_value, out)
10953        }
10954        "forEach" => {
10955            let cb = arg0(&args);
10956            array_walk(recv, |i, it| {
10957                host::invoke(
10958                    &cb,
10959                    vec![it, Value::Float(i as f64), this_value.clone()],
10960                    this_arg(&args, 1),
10961                )?;
10962                Ok(None::<()>)
10963            })?;
10964            Ok(Value::Undef)
10965        }
10966        "find" => {
10967            let items = array_items(recv);
10968            let cb = arg0(&args);
10969            for (i, it) in items.iter().enumerate() {
10970                let m = host::invoke(
10971                    &cb,
10972                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10973                    this_arg(&args, 1),
10974                )?;
10975                if with_host(|h| h.truthy(&m)) {
10976                    return Ok(it.clone());
10977                }
10978            }
10979            Ok(Value::Undef)
10980        }
10981        "findIndex" => {
10982            let items = array_items(recv);
10983            let cb = arg0(&args);
10984            for (i, it) in items.iter().enumerate() {
10985                let m = host::invoke(
10986                    &cb,
10987                    vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10988                    this_arg(&args, 1),
10989                )?;
10990                if with_host(|h| h.truthy(&m)) {
10991                    return Ok(Value::Float(i as f64));
10992                }
10993            }
10994            Ok(Value::Float(-1.0))
10995        }
10996        "some" => {
10997            let cb = arg0(&args);
10998            let thisarg = this_arg(&args, 1);
10999            let hit = array_walk(recv, |i, v| {
11000                let m = host::invoke(
11001                    &cb,
11002                    vec![v, Value::Float(i as f64), this_value.clone()],
11003                    thisarg.clone(),
11004                )?;
11005                Ok(with_host(|h| h.truthy(&m)).then_some(()))
11006            })?;
11007            Ok(Value::Bool(hit.is_some()))
11008        }
11009        "every" => {
11010            let cb = arg0(&args);
11011            let failed = array_walk(recv, |i, it| {
11012                let m = host::invoke(
11013                    &cb,
11014                    vec![it, Value::Float(i as f64), this_value.clone()],
11015                    this_arg(&args, 1),
11016                )?;
11017                Ok((!with_host(|h| h.truthy(&m))).then_some(()))
11018            })?;
11019            Ok(Value::Bool(failed.is_none()))
11020        }
11021        "reduce" => {
11022            let items = array_items(recv);
11023            let holes = absent_set(recv);
11024            let cb = arg0(&args);
11025            let acc;
11026            let mut start = 0;
11027            if args.len() >= 2 {
11028                acc = args[1].clone();
11029            } else {
11030                // With no seed the accumulator is the first PRESENT element, so a
11031                // leading run of holes is skipped rather than seeding `undefined`.
11032                match (0..items.len()).find(|i| !holes.contains(i)) {
11033                    Some(i) => {
11034                        acc = items[i].clone();
11035                        start = i + 1;
11036                    }
11037                    None => {
11038                        return Err(host::type_error(
11039                            "Reduce of empty array with no initial value",
11040                        ))
11041                    }
11042                }
11043            }
11044            // Each element is read LIVE at its index, so a callback that
11045            // shrinks the array is observed — the tail is skipped rather than
11046            // folded from a stale snapshot.
11047            let mut cur = acc;
11048            array_walk(recv, |i, it| {
11049                if i < start {
11050                    return Ok(None::<()>);
11051                }
11052                cur = host::invoke(
11053                    &cb,
11054                    vec![
11055                        std::mem::replace(&mut cur, Value::Undef),
11056                        it,
11057                        Value::Float(i as f64),
11058                        this_value.clone(),
11059                    ],
11060                    this_arg(&args, 1),
11061                )?;
11062                Ok(None::<()>)
11063            })?;
11064            Ok(cur)
11065        }
11066        "reduceRight" => {
11067            let cb = arg0(&args);
11068            let n = array_len(recv);
11069            let mut acc;
11070            let mut from = n; // one past the next index to process (walking down)
11071            if args.len() >= 2 {
11072                acc = args[1].clone();
11073            } else {
11074                let holes = absent_set(recv);
11075                match (0..n).rev().find(|i| !holes.contains(i)) {
11076                    Some(k) => {
11077                        acc = get_property(recv, &k.to_string())?;
11078                        from = k;
11079                    }
11080                    None => {
11081                        return Err(host::type_error(
11082                            "Reduce of empty array with no initial value",
11083                        ))
11084                    }
11085                }
11086            }
11087            // `acc` moves into the closure and back out on every step, so it
11088            // lives in an Option the closure can take from and refill.
11089            let mut slot = Some(acc);
11090            array_walk_rev(recv, from, |i, v| {
11091                let prev = slot.take().expect("accumulator is refilled each step");
11092                slot = Some(host::invoke(
11093                    &cb,
11094                    vec![prev, v, Value::Float(i as f64), this_value.clone()],
11095                    None,
11096                )?);
11097                Ok(None::<()>)
11098            })?;
11099            acc = slot.expect("accumulator is refilled each step");
11100            Ok(acc)
11101        }
11102        "findLast" => {
11103            let items = array_items(recv);
11104            let cb = arg0(&args);
11105            for i in (0..items.len()).rev() {
11106                let m = host::invoke(
11107                    &cb,
11108                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11109                    this_arg(&args, 1),
11110                )?;
11111                if with_host(|h| h.truthy(&m)) {
11112                    return Ok(items[i].clone());
11113                }
11114            }
11115            Ok(Value::Undef)
11116        }
11117        "findLastIndex" => {
11118            let items = array_items(recv);
11119            let cb = arg0(&args);
11120            for i in (0..items.len()).rev() {
11121                let m = host::invoke(
11122                    &cb,
11123                    vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11124                    this_arg(&args, 1),
11125                )?;
11126                if with_host(|h| h.truthy(&m)) {
11127                    return Ok(Value::Float(i as f64));
11128                }
11129            }
11130            Ok(Value::Float(-1.0))
11131        }
11132        // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11133        // and the holes are re-created at the tail — `[3,,1].sort()` is
11134        // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11135        "sort" => {
11136            let all = array_items(recv);
11137            let holes = absent_set(recv);
11138            let mut items: Vec<Value> = all
11139                .iter()
11140                .enumerate()
11141                .filter(|(i, _)| !holes.contains(i))
11142                .map(|(_, v)| v.clone())
11143                .collect();
11144            sort_values(&mut items, args.first())?;
11145            let present = items.len();
11146            // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11147            // captured at step 1: `Set` for each sorted element, then `Delete`
11148            // for the holes that followed them. Replacing the whole backing
11149            // vector instead discarded anything the COMPARATOR appended —
11150            // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11151            // original length with every pushed element gone.
11152            with_host(|h| {
11153                let len = all.len();
11154                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11155                    if a.len() < len {
11156                        a.resize(len, Value::Undef);
11157                    }
11158                    for (i, v) in items.into_iter().enumerate() {
11159                        a[i] = v;
11160                    }
11161                    for slot in a[present..len].iter_mut() {
11162                        *slot = Value::Undef;
11163                    }
11164                }
11165                h.install_holes(recv, (present..len).collect());
11166            });
11167            Ok(this_value.clone())
11168        }
11169        // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11170        "toSorted" => {
11171            let mut items = array_items(recv);
11172            sort_values(&mut items, args.first())?;
11173            Ok(with_host(|h| h.new_array(items)))
11174        }
11175        "toReversed" => {
11176            let mut items = array_items(recv);
11177            items.reverse();
11178            Ok(with_host(|h| h.new_array(items)))
11179        }
11180        "toSpliced" => {
11181            let mut items = array_items(recv);
11182            let len = items.len();
11183            let start = {
11184                let s = arg_num(&args, 0);
11185                if s < 0.0 {
11186                    ((len as f64 + s).max(0.0)) as usize
11187                } else {
11188                    (s as usize).min(len)
11189                }
11190            };
11191            let delete = if args.len() >= 2 {
11192                (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11193            } else if args.is_empty() {
11194                0
11195            } else {
11196                len - start
11197            };
11198            let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11199            items.splice(start..start + delete, inserts);
11200            Ok(with_host(|h| h.new_array(items)))
11201        }
11202        "with" => {
11203            let mut items = array_items(recv);
11204            let len = items.len() as i64;
11205            let rel = arg_num(&args, 0) as i64;
11206            let idx = if rel < 0 { len + rel } else { rel };
11207            if idx < 0 || idx >= len {
11208                return Err(host::range_error(&format!("Invalid index : {rel}")));
11209            }
11210            items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11211            Ok(with_host(|h| h.new_array(items)))
11212        }
11213        "flat" => {
11214            // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11215            // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11216            let raw = if args.is_empty() {
11217                1.0
11218            } else {
11219                arg_num(&args, 0)
11220            };
11221            let depth = if raw.is_nan() {
11222                0.0
11223            } else if raw.is_infinite() {
11224                raw
11225            } else {
11226                raw.trunc()
11227            };
11228            let mut out = Vec::new();
11229            flatten_into(recv, depth, &mut out)?;
11230            array_species_create(this_value, out)
11231        }
11232        // Live over the array (23.1.5.1): each step reads it as it is then.
11233        "keys" => Ok(array_iterator(recv, host::ArrayIterKind::Keys)),
11234        "values" | "@@iterator" => Ok(array_iterator(recv, host::ArrayIterKind::Values)),
11235        "entries" => Ok(array_iterator(recv, host::ArrayIterKind::Entries)),
11236        "splice" => array_splice(recv, args),
11237        // `Array.prototype.toString` IS `join()` with the default separator
11238        // (23.1.3.36), so it converts each element with `ToString` too — and
11239        // shares its cycle cut, which is the whole reason it must not call
11240        // `join_parts` directly: `ToString` of a nested array lands back here.
11241        "toString" => join_array(recv, ","),
11242        // An Array inherits from `Object.prototype` too, so the methods it does
11243        // not override resolve there. `[].hasOwnProperty` already read back as a
11244        // function through the property path, but CALLING it landed here and
11245        // threw `is not a function`.
11246        _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11247        _ => Err(host::type_error(&format!("{name} is not a function"))),
11248    }
11249}
11250
11251/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11252/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11253/// cut, which is not optional here: `ToString` of an element that is itself an
11254/// array re-enters through `toString`, so guarding only `join` left
11255/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11256/// process. On node v26.7.0 that expression is `""`.
11257fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11258    if !host::join_stack_push(recv) {
11259        return Ok(with_host(|h| h.new_str(String::new())));
11260    }
11261    // 23.1.3.18 step 6: the length is captured once, then each element is read
11262    // and STRINGIFIED before the next is read. Both halves are observable —
11263    // a getter or a `toString` that shrinks the array is seen by every later
11264    // element, which a read-all-then-convert pass misses.
11265    let parts = (|| -> Result<Vec<String>, String> {
11266        let len = array_len(recv);
11267        let mut out = Vec::with_capacity(len);
11268        for i in 0..len {
11269            let v = array_elem_live(recv, i)?;
11270            out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11271        }
11272        Ok(out)
11273    })();
11274    host::join_stack_pop();
11275    let s = parts?.join(sep);
11276    Ok(with_host(|h| h.new_str(s)))
11277}
11278
11279/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11280/// `null`/`undefined` element contributes the empty string, every other element
11281/// is `ToString(element)` — which for an object means invoking its `toString`,
11282/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11283/// `"[object Object]"`.
11284///
11285/// The all-primitive array — the overwhelmingly common one — is rendered under
11286/// a single host borrow; only an array actually holding an object pays for the
11287/// re-entrant per-element conversion.
11288fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11289    let fast = with_host(|h| {
11290        items
11291            .iter()
11292            .map(|x| match x {
11293                Value::Undef => Some(String::new()),
11294                _ if h.is_null(x) => Some(String::new()),
11295                // A SYMBOL element is primitive but has no `ToString`, so it must
11296                // fall through to the fallible path and throw there:
11297                // `[Symbol()].join()` is a TypeError on node v26.7.0.
11298                _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11299                _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11300                _ => None,
11301            })
11302            .collect::<Vec<_>>()
11303    });
11304    if fast.iter().all(Option::is_some) {
11305        return Ok(fast.into_iter().flatten().collect());
11306    }
11307    let mut out = Vec::with_capacity(items.len());
11308    for (x, p) in items.iter().zip(fast) {
11309        match p {
11310            Some(s) => out.push(s),
11311            None => {
11312                let s = host::to_string_value(x)?;
11313                out.push(with_host(|h| h.str_of(&s)));
11314            }
11315        }
11316    }
11317    Ok(out)
11318}
11319
11320/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11321/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11322/// the merge step; default order is by the string form of each element.
11323/// Propagates a comparator error.
11324///
11325/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11326/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11327/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11328/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11329/// number of times it is called is.
11330pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11331    // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11332    // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11333    // invoke path and reporting the generic `null is not a function`.
11334    let cmp = match cmp {
11335        Some(Value::Undef) => None,
11336        Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11337            // V8 renders the offending value with `NoSideEffectsToString`, not
11338            // with `util.inspect`: a string appears bare (`: x`) rather than
11339            // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11340            let shown = no_side_effects_string(v);
11341            return Err(host::type_error(&format!(
11342                "The comparison function must be either a function or undefined: {shown}"
11343            )));
11344        }
11345        other => other,
11346    };
11347    // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11348    // comparator — it sorts to the end after the defined values are ordered.
11349    // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11350    // node v26.7.0; the insertion sort called the comparator twice, on
11351    // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11352    // is `undefined`, so swapping keeps the defined values in input order.
11353    let mut defined = 0;
11354    for i in 0..items.len() {
11355        if !matches!(items[i], Value::Undef) {
11356            items.swap(defined, i);
11357            defined += 1;
11358        }
11359    }
11360    merge_sort(&mut items[..defined], cmp)
11361}
11362
11363/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11364/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11365/// `> 0` and the pair keeps its input order.
11366fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11367    match cmp {
11368        Some(cb) => {
11369            let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11370            Ok(with_host(|h| h.to_number(&v)))
11371        }
11372        None => {
11373            // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11374            // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11375            // from Rust's `String` order off the BMP.
11376            let x = with_host(|h| h.str_of(a));
11377            let y = with_host(|h| h.str_of(b));
11378            if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11379                Ok(1.0)
11380            } else {
11381                Ok(-1.0)
11382            }
11383        }
11384    }
11385}
11386
11387/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11388/// array cannot walk the native stack the JS comparator also runs on, and the
11389/// two buffers are swapped each pass instead of copied back.
11390fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11391    let n = items.len();
11392    if n < 2 {
11393        return Ok(());
11394    }
11395    let mut src = items.to_vec();
11396    let mut dst = src.clone();
11397    let mut width = 1;
11398    while width < n {
11399        let mut lo = 0;
11400        while lo < n {
11401            let mid = (lo + width).min(n);
11402            let hi = (lo + 2 * width).min(n);
11403            merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11404            lo = hi;
11405        }
11406        std::mem::swap(&mut src, &mut dst);
11407        width *= 2;
11408    }
11409    items.clone_from_slice(&src);
11410    Ok(())
11411}
11412
11413/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11414/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11415/// keeps the two `k:0` entries in input order, as node does.
11416fn merge(
11417    left: &[Value],
11418    right: &[Value],
11419    out: &mut [Value],
11420    cmp: Option<&Value>,
11421) -> Result<(), String> {
11422    let (mut i, mut j, mut k) = (0, 0, 0);
11423    while i < left.len() && j < right.len() {
11424        if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11425            out[k] = right[j].clone();
11426            j += 1;
11427        } else {
11428            out[k] = left[i].clone();
11429            i += 1;
11430        }
11431        k += 1;
11432    }
11433    for v in left[i..].iter().chain(&right[j..]) {
11434        out[k] = v.clone();
11435        k += 1;
11436    }
11437    Ok(())
11438}
11439
11440/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11441/// f64 so `Infinity` (full flatten) and finite counts share one path.
11442///
11443/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11444/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11445/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11446/// stack floor the VM does, so the answer is a catchable error rather than the
11447/// `fatal runtime error: stack overflow` abort this used to produce.
11448/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11449/// elements because each level tests `HasProperty` before recursing, so a hole
11450/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11451fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11452    if host::stack_exhausted() {
11453        return Err(host::stack_overflow_error());
11454    }
11455    let items = array_items(src);
11456    let holes = absent_set(src);
11457    for (i, it) in items.into_iter().enumerate() {
11458        if holes.contains(&i) {
11459            continue;
11460        }
11461        let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11462        if nested {
11463            flatten_into(&it, depth - 1.0, out)?;
11464        } else {
11465            out.push(it);
11466        }
11467    }
11468    Ok(())
11469}
11470
11471fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11472    let len = array_len(recv);
11473    let start = {
11474        let s = arg_num(&args, 0);
11475        if s < 0.0 {
11476            ((len as f64 + s).max(0.0)) as usize
11477        } else {
11478            (s as usize).min(len)
11479        }
11480    };
11481    let delete = if args.len() >= 2 {
11482        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11483    } else {
11484        len - start
11485    };
11486    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11487    let inserted = inserts.len();
11488    // The receiver's holes shift by (inserted - deleted) past the cut, and the
11489    // ones inside the cut move into the RETURNED array at their offset there.
11490    let holes = hole_set(recv);
11491    let removed = with_host(|h| {
11492        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11493            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11494            removed
11495        } else {
11496            Vec::new()
11497        }
11498    });
11499    let spliced = with_host(|h| {
11500        h.install_holes(
11501            recv,
11502            holes
11503                .iter()
11504                .filter_map(|&i| {
11505                    if i < start {
11506                        Some(i)
11507                    } else if i < start + delete {
11508                        None
11509                    } else {
11510                        Some(i - delete + inserted)
11511                    }
11512                })
11513                .collect(),
11514        );
11515        (removed, holes.clone())
11516    });
11517    // The REMOVED elements come back as an array of the receiver's species
11518    // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11519    let (removed, holes) = spliced;
11520    let out = array_species_create(recv, removed)?;
11521    with_host(|h| {
11522        h.install_holes(
11523            &out,
11524            holes
11525                .iter()
11526                .filter(|&&i| i >= start && i < start + delete)
11527                .map(|&i| i - start)
11528                .collect(),
11529        );
11530    });
11531    Ok(out)
11532}
11533
11534fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11535    let norm = |v: f64| -> usize {
11536        if v < 0.0 {
11537            ((len as f64 + v).max(0.0)) as usize
11538        } else {
11539            (v as usize).min(len)
11540        }
11541    };
11542    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11543        0
11544    } else {
11545        norm(arg_num(args, 0))
11546    };
11547    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11548        len
11549    } else {
11550        norm(arg_num(args, 1))
11551    };
11552    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11553    // never a reversed one: JS `slice` clamps `end` up to `start`.
11554    (lo, hi.max(lo))
11555}
11556
11557/// The argument positions each `String.prototype` method coerces with
11558/// `ToNumber` rather than `ToString`. Everything not listed is a string
11559/// position — which matters only for a SYMBOL argument, the one value both
11560/// conversions refuse, and refuse with different wording.
11561///
11562/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11563/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11564/// pair the other way round (a length then a pad string).
11565const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11566    ("at", &[0]),
11567    ("charAt", &[0]),
11568    ("charCodeAt", &[0]),
11569    ("codePointAt", &[0]),
11570    ("endsWith", &[1]),
11571    ("includes", &[1]),
11572    ("indexOf", &[1]),
11573    ("lastIndexOf", &[1]),
11574    ("padEnd", &[0]),
11575    ("padStart", &[0]),
11576    ("repeat", &[0]),
11577    ("slice", &[0, 1]),
11578    ("split", &[1]),
11579    ("startsWith", &[1]),
11580    ("substr", &[0, 1]),
11581    ("substring", &[0, 1]),
11582];
11583
11584/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11585/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11586/// rendered `Symbol(d)` into the result — silently, which is the shape of
11587/// mistake that makes a symbol key leak into text.
11588fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11589    let numeric = STRING_METHOD_NUMERIC_ARGS
11590        .iter()
11591        .find(|(m, _)| *m == name)
11592        .map(|(_, ps)| *ps)
11593        .unwrap_or(&[]);
11594    for (i, a) in args.iter().enumerate() {
11595        if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11596            let kind = if numeric.contains(&i) {
11597                "number"
11598            } else {
11599                "string"
11600            };
11601            return Err(host::type_error(&format!(
11602                "Cannot convert a Symbol value to a {kind}"
11603            )));
11604        }
11605    }
11606    Ok(())
11607}
11608
11609/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11610/// reads them: a numeric position through `ToNumber`, every other through
11611/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11612/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11613/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11614///
11615/// The positions that must NOT be coerced are the ones with their own protocol:
11616/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11617/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11618/// `replace`/`replaceAll`. Each of those already has a path that handles the
11619/// value as an object, and stringifying it first would take that path away.
11620/// The argument positions each `Array.prototype` method coerces with
11621/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11622/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11623/// are stored as given, and `indexOf`/`includes` compare their first argument
11624/// without converting it.
11625const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11626    ("at", &[0]),
11627    ("copyWithin", &[0, 1, 2]),
11628    ("fill", &[1, 2]),
11629    ("flat", &[0]),
11630    ("includes", &[1]),
11631    ("indexOf", &[1]),
11632    ("lastIndexOf", &[1]),
11633    ("slice", &[0, 1]),
11634    ("splice", &[0, 1]),
11635    ("toSpliced", &[0, 1]),
11636    ("with", &[0]),
11637];
11638
11639/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11640/// number, and is deliberately absent.
11641const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11642    ("toExponential", &[0]),
11643    ("toFixed", &[0]),
11644    ("toPrecision", &[0]),
11645    ("toString", &[0]),
11646];
11647
11648/// Replace the listed argument positions with their `ToNumber` value, running a
11649/// user `valueOf` and propagating a throw from it. Every one of these read the
11650/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11651/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11652/// was a RangeError.
11653/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11654fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11655    let v = args.get(i).cloned().unwrap_or(Value::Undef);
11656    let p = host::to_primitive(&v, "number")?;
11657    Ok(with_host(|h| h.to_number(&p)))
11658}
11659
11660fn coerce_numeric_args(
11661    table: &[(&str, &[usize])],
11662    name: &str,
11663    mut args: Vec<Value>,
11664) -> Result<Vec<Value>, String> {
11665    let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11666        return Ok(args);
11667    };
11668    for &i in *positions {
11669        let Some(a) = args.get(i) else { continue };
11670        if matches!(a, Value::Undef) {
11671            continue;
11672        }
11673        let p = host::to_primitive(a, "number")?;
11674        args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11675    }
11676    Ok(args)
11677}
11678
11679/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11680/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11681/// matches at position 0.
11682fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11683    let src = if matches!(v, Value::Undef) {
11684        String::new()
11685    } else {
11686        with_host(|h| h.str_of(v))
11687    };
11688    let fv = with_host(|h| h.new_str(flags.to_string()));
11689    let sv = with_host(|h| h.new_str(src));
11690    regexp_ctor(&[sv, fv])
11691}
11692
11693fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11694    let numeric = STRING_METHOD_NUMERIC_ARGS
11695        .iter()
11696        .find(|(m, _)| *m == name)
11697        .map(|(_, ps)| *ps)
11698        .unwrap_or(&[]);
11699    let protocol = match name {
11700        "replace" | "replaceAll" => Some("@@replace"),
11701        "split" => Some("@@split"),
11702        "match" => Some("@@match"),
11703        "matchAll" => Some("@@matchAll"),
11704        "search" => Some("@@search"),
11705        // These three do not CONSUME `Symbol.match`, they reject a value that
11706        // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11707        // the object intact so that check still sees one; stringifying first
11708        // turned the TypeError into an ordinary search.
11709        "startsWith" | "endsWith" | "includes" => Some("@@match"),
11710        _ => None,
11711    };
11712    let mut out = Vec::with_capacity(args.len());
11713    for (i, a) in args.into_iter().enumerate() {
11714        if matches!(a, Value::Undef) {
11715            out.push(a);
11716            continue;
11717        }
11718        if numeric.contains(&i) {
11719            let p = host::to_primitive(&a, "number")?;
11720            out.push(Value::Float(with_host(|h| h.to_number(&p))));
11721            continue;
11722        }
11723        // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11724        // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11725        // regexp and coerces like anything else. The consuming protocols use
11726        // `GetMethod`, which additionally requires a callable.
11727        let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11728        let carries = |p: &str| match host::protocol_lookup(&a, p) {
11729            Ok(Some(m)) => {
11730                if is_regexp_like {
11731                    with_host(|h| h.truthy(&m))
11732                } else {
11733                    with_host(|h| host::is_callable(h, &m))
11734                }
11735            }
11736            _ => false,
11737        };
11738        let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11739            || (i == 0 && protocol.is_some_and(carries))
11740            || (i == 1
11741                && matches!(name, "replace" | "replaceAll")
11742                && with_host(|h| host::is_callable(h, &a)));
11743        if exempt {
11744            out.push(a);
11745            continue;
11746        }
11747        out.push(host::to_string_value(&a)?);
11748    }
11749    Ok(out)
11750}
11751
11752fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11753    reject_symbol_args(name, &args)?;
11754    let args = coerce_string_args(name, args)?;
11755    // Every index-bearing method below counts UTF-16 code units, so they all
11756    // work off this one decoding rather than off `s.chars()` (code points),
11757    // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11758    let u = crate::utf16::Units::of(s);
11759    match name {
11760        // `for…of` / spread over a string iterates CODE POINTS, not code units:
11761        // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11762        // is the one string operation that is specified in chars, so it stays
11763        // on `s.chars()` on purpose — do not "fix" it to match the others.
11764        "@@iterator" => {
11765            let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11766            Ok(with_host(|h| {
11767                h.alloc(JsObj::Iter {
11768                    items,
11769                    idx: 0,
11770                    array: None,
11771                })
11772            }))
11773        }
11774        "toUpperCase" => Ok(new_s(s.to_uppercase())),
11775        "toLowerCase" => Ok(new_s(s.to_lowercase())),
11776        // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11777        // from the plain forms only for the locale-specific mappings (Turkish
11778        // dotless i, Lithuanian accents); with no locale argument they are the
11779        // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11780        // `to_lowercase`. They threw `is not a function` before, so the common
11781        // no-argument call — the only form this runtime can answer, since it
11782        // carries no ICU — failed outright rather than agreeing with node.
11783        // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11784        // is `'i'` here and `'ı'` in node.
11785        // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11786        // has no locale rendering. Missing it made an ARRAY of strings fail too,
11787        // since `Array.prototype.toLocaleString` invokes it per element.
11788        "toLocaleString" => Ok(new_s(s.to_string())),
11789        "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11790        "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11791        // Locale comparison (ASCII approximation of ICU collation): primary by
11792        // case-folded order, then lowercase sorts before uppercase at a tie.
11793        "localeCompare" => {
11794            let other = with_host(|h| h.str_of(&arg0(&args)));
11795            let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11796            let r = match la.cmp(&lb) {
11797                std::cmp::Ordering::Less => -1.0,
11798                std::cmp::Ordering::Greater => 1.0,
11799                std::cmp::Ordering::Equal => {
11800                    let mut t = 0.0;
11801                    for (ca, cb) in s.chars().zip(other.chars()) {
11802                        if ca != cb {
11803                            t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11804                            break;
11805                        }
11806                    }
11807                    t
11808                }
11809            };
11810            Ok(Value::Float(r))
11811        }
11812        // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11813        //
11814        // This used to return the receiver unchanged and only validate the FORM
11815        // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11816        // one code point) and `"Å"` (NFD, two) stayed distinct under
11817        // `.normalize()`, so the standard way to compare Unicode text for
11818        // canonical equivalence silently answered `false`, and `NFKC` never
11819        // folded a compatibility character (`"fi"` stayed one code point instead
11820        // of becoming `"fi"`). The tables come from `unicode-normalization`.
11821        "normalize" => {
11822            use unicode_normalization::UnicodeNormalization;
11823            let form = match args.first() {
11824                Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11825                _ => "NFC".to_string(),
11826            };
11827            let out = match form.as_str() {
11828                "NFC" => s.nfc().collect::<String>(),
11829                "NFD" => s.nfd().collect::<String>(),
11830                "NFKC" => s.nfkc().collect::<String>(),
11831                "NFKD" => s.nfkd().collect::<String>(),
11832                _ => {
11833                    return Err(host::range_error(
11834                        "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11835                    ))
11836                }
11837            };
11838            Ok(new_s(out))
11839        }
11840        // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11841        // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11842        // value this runtime can hold is well-formed by construction and
11843        // `toWellFormed` has nothing to replace. Both answers are therefore
11844        // exact for every string that survives storage; the one case node
11845        // answers differently is a surrogate half extracted by `charAt`/`slice`,
11846        // which is already `U+FFFD` here — the documented lone-surrogate
11847        // boundary in `utf16`, not a separate gap.
11848        "isWellFormed" => Ok(Value::Bool(true)),
11849        "toWellFormed" => Ok(new_s(s.to_string())),
11850        // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11851        "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11852        "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11853        "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11854        "toString" | "valueOf" => Ok(new_s(s.to_string())),
11855        "charAt" => {
11856            let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11857            Ok(new_s(at.unwrap_or_default()))
11858        }
11859        "at" => {
11860            let n = arg_num(&args, 0);
11861            // A negative position counts back from the end; `NaN` is 0. An
11862            // infinite position is out of range in either direction.
11863            let i = if n.is_nan() {
11864                Some(0i64)
11865            } else if n.is_finite() {
11866                let i = n.trunc() as i64;
11867                Some(if i < 0 { i + u.len() as i64 } else { i })
11868            } else {
11869                None
11870            };
11871            match i
11872                .and_then(|i| usize::try_from(i).ok())
11873                .and_then(|i| u.unit_str(i))
11874            {
11875                Some(c) => Ok(new_s(c)),
11876                None => Ok(Value::Undef),
11877            }
11878        }
11879        // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11880        // astral character, not the character. `codePointAt` looks ahead one
11881        // unit and reports the whole scalar when the pair is well formed. They
11882        // agree everywhere on the BMP, which is why they used to share an arm.
11883        // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11884        // `codePointAt` yields `undefined` (measured on node v26.7.0).
11885        "charCodeAt" => {
11886            let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11887            Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11888        }
11889        "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11890            Some(cp) => Ok(Value::Float(f64::from(cp))),
11891            None => Ok(Value::Undef),
11892        },
11893        // The search quartet all honor their optional position argument.
11894        // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11895        // parameterCount walks a query string with exactly that call.
11896        "indexOf" => {
11897            let needle = needle_units(&args);
11898            let from = clamp_pos(arg_num(&args, 1), u.len());
11899            Ok(Value::Float(
11900                search_from(u.as_slice(), needle.as_slice(), from)
11901                    .map(|i| i as f64)
11902                    .unwrap_or(-1.0),
11903            ))
11904        }
11905        "lastIndexOf" => {
11906            let needle = needle_units(&args);
11907            // An absent or NaN position means "search the whole string".
11908            let n = arg_num(&args, 1);
11909            let upto = if n.is_nan() {
11910                u.len()
11911            } else {
11912                clamp_pos(n, u.len())
11913            };
11914            Ok(Value::Float(
11915                search_last(u.as_slice(), needle.as_slice(), upto)
11916                    .map(|i| i as f64)
11917                    .unwrap_or(-1.0),
11918            ))
11919        }
11920        // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11921        // argument outright, and `IsRegExp` is what decides — so an object
11922        // advertising `Symbol.match` is rejected too. None of them checked.
11923        "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11924            Err(host::type_error(&format!(
11925                "First argument to String.prototype.{name} must not be a regular expression"
11926            )))
11927        }
11928        "includes" => {
11929            let needle = needle_units(&args);
11930            let from = clamp_pos(arg_num(&args, 1), u.len());
11931            Ok(Value::Bool(
11932                search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11933            ))
11934        }
11935        "startsWith" => {
11936            let needle = needle_units(&args);
11937            let from = clamp_pos(arg_num(&args, 1), u.len());
11938            Ok(Value::Bool(
11939                u.as_slice()[from..].starts_with(needle.as_slice()),
11940            ))
11941        }
11942        "endsWith" => {
11943            let needle = needle_units(&args);
11944            // The 2nd argument is where the string is treated as ENDING.
11945            let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11946                u.len()
11947            } else {
11948                clamp_pos(arg_num(&args, 1), u.len())
11949            };
11950            Ok(Value::Bool(
11951                u.as_slice()[..end].ends_with(needle.as_slice()),
11952            ))
11953        }
11954        "slice" => {
11955            let (lo, hi) = slice_bounds(&args, u.len());
11956            Ok(new_s(u.slice(lo, hi)))
11957        }
11958        "substring" => {
11959            let mut a = arg_num(&args, 0).max(0.0) as usize;
11960            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11961                u.len()
11962            } else {
11963                (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11964            };
11965            a = a.min(u.len());
11966            if a > b {
11967                std::mem::swap(&mut a, &mut b);
11968            }
11969            Ok(new_s(u.slice(a, b)))
11970        }
11971        "substr" => {
11972            // A negative start counts from the end: max(len + start, 0).
11973            let len = u.len() as i64;
11974            let mut start = arg_num(&args, 0) as i64;
11975            if start < 0 {
11976                start = (len + start).max(0);
11977            }
11978            let start = (start as usize).min(u.len());
11979            let count = if args.len() >= 2 {
11980                arg_num(&args, 1).max(0.0) as usize
11981            } else {
11982                u.len()
11983            };
11984            let end = start.saturating_add(count).min(u.len());
11985            Ok(new_s(u.slice(start, end)))
11986        }
11987        "repeat" => {
11988            let n = arg_num(&args, 0);
11989            // `RangeError`, not `TypeError`, and the count is named:
11990            // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11991            if n < 0.0 || !n.is_finite() {
11992                return Err(host::range_error(&format!(
11993                    "Invalid count value: {}",
11994                    host::fmt_number(n)
11995                )));
11996            }
11997            // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11998            // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11999            // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
12000            // is `RangeError: Invalid string length`.
12001            if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
12002                return Err(host::invalid_string_length());
12003            }
12004            Ok(new_s(s.repeat(n as usize)))
12005        }
12006        "concat" => {
12007            let mut out = s.to_string();
12008            for a in &args {
12009                out.push_str(&with_host(|h| h.str_of(a)));
12010            }
12011            Ok(new_s(out))
12012        }
12013        "padStart" => Ok(new_s(pad(s, &args, true)?)),
12014        "padEnd" => Ok(new_s(pad(s, &args, false)?)),
12015        // Regex-taking string methods: dispatch to the regexp module when the
12016        // argument is a RegExp; otherwise keep the plain-string behavior.
12017        // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
12018        // consults `Symbol.replace`, so a non-global regexp is a TypeError even
12019        // though a RegExp does define that method. Delegating first skipped the
12020        // check and silently did a single replacement.
12021        "replaceAll"
12022            if is_regexp_arg(&arg0(&args))
12023                && !with_host(
12024                    |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
12025                ) =>
12026        {
12027            Err(host::type_error(
12028                "String.prototype.replaceAll called with a non-global RegExp argument",
12029            ))
12030        }
12031        "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
12032            if symbol_protocol(
12033                &arg0(&args),
12034                match name {
12035                    "match" => "@@match",
12036                    "matchAll" => "@@matchAll",
12037                    "search" => "@@search",
12038                    "split" => "@@split",
12039                    _ => "@@replace",
12040                },
12041            )
12042            .is_some() =>
12043        {
12044            let sym = match name {
12045                "match" => "@@match",
12046                "matchAll" => "@@matchAll",
12047                "search" => "@@search",
12048                "split" => "@@split",
12049                _ => "@@replace",
12050            };
12051            let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
12052            let sv = with_host(|h| h.new_str(s.to_string()));
12053            let mut rest = vec![sv];
12054            rest.extend(args.iter().skip(1).cloned());
12055            host::invoke(&f, rest, Some(arg0(&args)))
12056        }
12057        // 22.1.3.13/14: a non-RegExp argument is turned INTO one
12058        // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
12059        // answered `null` for every string argument, which reads as "no match"
12060        // — the one answer a caller cannot tell from a real failure.
12061        // `matchAll` builds its with `g`, which 22.1.3.14 requires.
12062        "match" => {
12063            let a = arg0(&args);
12064            let re = if is_regexp_arg(&a) {
12065                a
12066            } else {
12067                regexp_from_arg(&a, "")?
12068            };
12069            crate::regexp::str_match(s, &re)
12070        }
12071        "matchAll" => {
12072            let a = arg0(&args);
12073            let re = if is_regexp_arg(&a) {
12074                a
12075            } else {
12076                regexp_from_arg(&a, "g")?
12077            };
12078            crate::regexp::str_match_all(s, &re)
12079        }
12080        "search" => {
12081            if is_regexp_arg(&arg0(&args)) {
12082                crate::regexp::str_search(s, &arg0(&args))
12083            } else {
12084                // 22.1.3.17 builds a RegExp from the argument, so a
12085                // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
12086                // 1. The substring approximation this replaces agreed only for
12087                // a literal needle, and answered -1 for an absent argument
12088                // where the empty pattern matches at 0.
12089                let re = regexp_from_arg(&arg0(&args), "")?;
12090                crate::regexp::str_search(s, &re)
12091            }
12092        }
12093        "replace" => {
12094            let pat = arg0(&args);
12095            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12096            if is_regexp_arg(&pat) {
12097                crate::regexp::str_replace_regex(s, &pat, &repl, false)
12098            } else if with_host(|h| host::is_callable(h, &repl)) {
12099                Ok(new_s(replace_str_fn(
12100                    s,
12101                    &with_host(|h| h.str_of(&pat)),
12102                    &repl,
12103                    false,
12104                )?))
12105            } else {
12106                let from = with_host(|h| h.str_of(&pat));
12107                let to = with_host(|h| h.str_of(&repl));
12108                Ok(new_s(replace_str_plain(s, &from, &to, false)))
12109            }
12110        }
12111        "replaceAll" => {
12112            let pat = arg0(&args);
12113            let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12114            if is_regexp_arg(&pat) {
12115                // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12116                // because `replaceAll` cannot honour "all" without `g`. This
12117                // used to replace only the first match and say nothing.
12118                let global = with_host(|h| match h.get(&pat) {
12119                    Some(JsObj::RegExp(r)) => r.global,
12120                    _ => true,
12121                });
12122                if !global {
12123                    return Err(host::type_error(
12124                        "String.prototype.replaceAll called with a non-global RegExp argument",
12125                    ));
12126                }
12127                crate::regexp::str_replace_regex(s, &pat, &repl, true)
12128            } else if with_host(|h| host::is_callable(h, &repl)) {
12129                Ok(new_s(replace_str_fn(
12130                    s,
12131                    &with_host(|h| h.str_of(&pat)),
12132                    &repl,
12133                    true,
12134                )?))
12135            } else {
12136                let from = with_host(|h| h.str_of(&pat));
12137                let to = with_host(|h| h.str_of(&repl));
12138                Ok(new_s(replace_str_plain(s, &from, &to, true)))
12139            }
12140        }
12141        "split" => {
12142            if is_regexp_arg(&arg0(&args)) {
12143                let limit = args
12144                    .get(1)
12145                    .filter(|v| !matches!(v, Value::Undef))
12146                    .map(|v| with_host(|h| h.to_number(v)) as usize);
12147                return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12148            }
12149            let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12150                vec![new_s(s.to_string())]
12151            } else {
12152                let sep = with_host(|h| h.str_of(&args[0]));
12153                if sep.is_empty() {
12154                    // `split('')` yields one element per code UNIT, so an astral
12155                    // character becomes its two surrogate halves.
12156                    (0..u.len())
12157                        .filter_map(|i| u.unit_str(i))
12158                        .map(new_s)
12159                        .collect()
12160                } else {
12161                    s.split(&sep as &str)
12162                        .map(|p| new_s(p.to_string()))
12163                        .collect()
12164                }
12165            };
12166            // Optional limit: keep at most `limit` substrings.
12167            if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12168                let n = with_host(|h| h.to_number(lim));
12169                if n.is_finite() && n >= 0.0 {
12170                    parts.truncate(n as usize);
12171                }
12172            }
12173            Ok(with_host(|h| h.new_array(parts)))
12174        }
12175        _ => Err(host::type_error(&format!("{name} is not a function"))),
12176    }
12177}
12178
12179/// GetSubstitution (22.1.3.19) for a STRING search value.
12180///
12181/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12182/// the pattern is a regexp or a plain string, but the string path here did a
12183/// raw `str::replace` and passed the template through verbatim — so
12184/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12185/// regexp path has always expanded them.
12186///
12187/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12188/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12189/// what node does.
12190fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12191    let chars: Vec<char> = templ.chars().collect();
12192    let mut out = String::new();
12193    let mut i = 0;
12194    while i < chars.len() {
12195        if chars[i] == '$' && i + 1 < chars.len() {
12196            match chars[i + 1] {
12197                '$' => {
12198                    out.push('$');
12199                    i += 2;
12200                    continue;
12201                }
12202                '&' => {
12203                    out.push_str(matched);
12204                    i += 2;
12205                    continue;
12206                }
12207                '`' => {
12208                    out.push_str(&subject[..position]);
12209                    i += 2;
12210                    continue;
12211                }
12212                '\'' => {
12213                    out.push_str(&subject[position + matched.len()..]);
12214                    i += 2;
12215                    continue;
12216                }
12217                _ => {}
12218            }
12219        }
12220        out.push(chars[i]);
12221        i += 1;
12222    }
12223    out
12224}
12225
12226/// `replace`/`replaceAll` with a string pattern and a string replacement,
12227/// expanding each match's `$` patterns against its own position.
12228fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12229    if from.is_empty() && !all {
12230        return format!("{}{s}", substitute_plain(to, "", 0, s));
12231    }
12232    let mut out = String::new();
12233    let mut rest = 0usize;
12234    while let Some(rel) = s[rest..].find(from) {
12235        let at = rest + rel;
12236        out.push_str(&s[rest..at]);
12237        out.push_str(&substitute_plain(to, from, at, s));
12238        rest = at + from.len();
12239        if !all {
12240            break;
12241        }
12242        // An empty pattern matches between every character; step one along so
12243        // the scan terminates.
12244        if from.is_empty() {
12245            if rest >= s.len() {
12246                break;
12247            }
12248            let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12249            out.push_str(&s[rest..rest + step]);
12250            rest += step;
12251        }
12252    }
12253    out.push_str(&s[rest..]);
12254    out
12255}
12256
12257fn new_s(s: String) -> Value {
12258    with_host(|h| h.new_str(s))
12259}
12260
12261/// Where a forward `indexOf`/`includes` search starts, given the optional
12262/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12263/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12264/// or past the end finds nothing, which callers report as `-1` / `false`.
12265pub(crate) fn search_start(n: f64, len: usize) -> usize {
12266    if n.is_nan() {
12267        return 0;
12268    }
12269    let n = n.trunc();
12270    if n >= 0.0 {
12271        if n >= len as f64 {
12272            len
12273        } else {
12274            n as usize
12275        }
12276    } else {
12277        let from_end = len as f64 + n;
12278        if from_end <= 0.0 {
12279            0
12280        } else {
12281            from_end as usize
12282        }
12283    }
12284}
12285
12286/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12287/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12288/// the last element — which is why this takes an `Option` rather than reading
12289/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12290/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12291pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12292    if len == 0 {
12293        return None;
12294    }
12295    let n = match from {
12296        None => return Some(len - 1),
12297        Some(v) if v.is_nan() => 0.0,
12298        Some(v) => v.trunc(),
12299    };
12300    if n >= 0.0 {
12301        Some(if n >= len as f64 { len - 1 } else { n as usize })
12302    } else {
12303        let k = len as f64 + n;
12304        if k < 0.0 {
12305            None
12306        } else {
12307            Some(k as usize)
12308        }
12309    }
12310}
12311
12312/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12313/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12314fn clamp_pos(n: f64, len: usize) -> usize {
12315    if n.is_nan() || n <= 0.0 {
12316        0
12317    } else if n >= len as f64 {
12318        len
12319    } else {
12320        n.trunc() as usize
12321    }
12322}
12323
12324/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12325/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12326/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12327fn unit_pos(n: f64) -> Option<usize> {
12328    if n.is_nan() {
12329        Some(0)
12330    } else if n < 0.0 || !n.is_finite() {
12331        None
12332    } else {
12333        Some(n.trunc() as usize)
12334    }
12335}
12336
12337/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12338/// the needle is compared in the same alphabet the haystack is indexed by.
12339fn needle_units(args: &[Value]) -> crate::utf16::Units {
12340    crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12341}
12342
12343/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12344/// needle matches at `from` itself, as JS specifies.
12345fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12346    if needle.is_empty() {
12347        return Some(from.min(hay.len()));
12348    }
12349    if needle.len() > hay.len() {
12350        return None;
12351    }
12352    (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12353}
12354
12355/// The highest index `<= upto` at which `needle` occurs in `hay`.
12356fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12357    if needle.is_empty() {
12358        return Some(upto.min(hay.len()));
12359    }
12360    if needle.len() > hay.len() {
12361        return None;
12362    }
12363    let last = hay.len() - needle.len();
12364    (0..=upto.min(last))
12365        .rev()
12366        .find(|&i| &hay[i..i + needle.len()] == needle)
12367}
12368
12369fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12370    let target_f = arg_num(args, 0);
12371    let target = if target_f.is_finite() && target_f > 0.0 {
12372        target_f as usize
12373    } else {
12374        0
12375    };
12376    // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12377    // is `'-𝒳'` in node, not `'--𝒳'`.
12378    let cur = crate::utf16::len(s);
12379    if cur >= target {
12380        return Ok(s.to_string());
12381    }
12382    let filler = if args.len() >= 2 {
12383        with_host(|h| h.str_of(&args[1]))
12384    } else {
12385        " ".to_string()
12386    };
12387    if filler.is_empty() {
12388        return Ok(s.to_string());
12389    }
12390    // Checked only AFTER the two short-circuits, which is the order V8 uses:
12391    // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12392    // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12393    if target_f > host::MAX_STRING_LENGTH as f64 {
12394        return Err(host::invalid_string_length());
12395    }
12396    let need = target - cur;
12397    let fill = crate::utf16::Units::of(&filler);
12398    // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12399    // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12400    // (see src/utf16.rs).
12401    let units: Vec<u16> = (0..need)
12402        .filter_map(|i| fill.unit(i % fill.len()))
12403        .collect();
12404    let padding = crate::utf16::to_string_lossy(&units);
12405    Ok(if start {
12406        format!("{padding}{s}")
12407    } else {
12408        format!("{s}{padding}")
12409    })
12410}
12411
12412/// V8's radix rejection, shared by `Number.prototype.toString` and
12413/// `BigInt.prototype.toString` — one string, because they are one message and
12414/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12415/// must be").
12416const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12417
12418/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12419fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12420    match name {
12421        "toString" => {
12422            let radix = match args.first() {
12423                None | Some(Value::Undef) => 10,
12424                Some(_) => {
12425                    let t = arg_num(&args, 0).trunc();
12426                    if !(2.0..=36.0).contains(&t) {
12427                        return Err(host::range_error(RADIX_RANGE));
12428                    }
12429                    t as u32
12430                }
12431            };
12432            Ok(new_s(b.to_str_radix(radix)))
12433        }
12434        // `BigInt.prototype.toLocaleString` groups thousands like the Number
12435        // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12436        // returning the bare digits made it the only numeric type that skipped
12437        // grouping. Same en-US-shaped output as `Number.prototype`, formatted
12438        // from the EXACT digits; `options` is honored, `locales` ignored (no
12439        // ICU here).
12440        "toLocaleString" => {
12441            let digits = b.magnitude().to_string();
12442            let neg = b.sign() == num_bigint::Sign::Minus;
12443            let opts = args.get(1).cloned().unwrap_or(Value::Undef);
12444            Ok(new_s(crate::numfmt::to_locale_string(
12445                crate::numfmt::Num::BigInt(&digits, neg),
12446                &opts,
12447            )?))
12448        }
12449        "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12450        _ => Err(host::type_error(&format!("{name} is not a function"))),
12451    }
12452}
12453
12454fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12455    let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12456    match name {
12457        "toFixed" => {
12458            let digits = arg_num(&args, 0);
12459            if !(0.0..=100.0).contains(&digits.trunc()) {
12460                return Err(host::range_error(
12461                    "toFixed() digits argument must be between 0 and 100",
12462                ));
12463            }
12464            Ok(new_s(to_fixed(n, digits as usize)))
12465        }
12466        "toExponential" => {
12467            // `undefined` (or a missing argument) selects the shortest form.
12468            let f = match args.first() {
12469                None | Some(Value::Undef) => None,
12470                Some(_) => {
12471                    let d = arg_num(&args, 0).trunc();
12472                    if !(0.0..=100.0).contains(&d) {
12473                        return Err(host::range_error(
12474                            "toExponential() argument must be between 0 and 100",
12475                        ));
12476                    }
12477                    Some(d as usize)
12478                }
12479            };
12480            Ok(new_s(to_exponential(n, f)))
12481        }
12482        "toString" => {
12483            // An out-of-range radix THROWS; it does not silently fall back to
12484            // base 10. `(1).toString(37)` returned "1" here, so a support probe
12485            // was told every radix worked.
12486            let radix = match args.first() {
12487                None | Some(Value::Undef) => 10,
12488                Some(_) => {
12489                    let r = arg_num(&args, 0);
12490                    let t = r.trunc();
12491                    if !(2.0..=36.0).contains(&t) {
12492                        return Err(host::range_error(RADIX_RANGE));
12493                    }
12494                    t as u32
12495                }
12496            };
12497            if radix == 10 {
12498                Ok(new_s(host::fmt_number(n)))
12499            } else {
12500                Ok(new_s(to_radix(n, radix)))
12501            }
12502        }
12503        "toPrecision" => {
12504            // `undefined` (or a missing argument) behaves like `toString()`.
12505            match args.first() {
12506                None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12507                Some(_) => {
12508                    let p = arg_num(&args, 0).trunc();
12509                    if !(1.0..=100.0).contains(&p) {
12510                        return Err(host::range_error(
12511                            "toPrecision() argument must be between 1 and 100",
12512                        ));
12513                    }
12514                    Ok(new_s(to_precision(n, p as usize)))
12515                }
12516            }
12517        }
12518        // The `options` argument (digit options, grouping, percent/currency
12519        // style) is honored in the en-US shape; see `crate::numfmt`.
12520        "toLocaleString" => {
12521            let opts = args.get(1).cloned().unwrap_or(Value::Undef);
12522            Ok(new_s(crate::numfmt::to_locale_string(
12523                crate::numfmt::Num::Float(n),
12524                &opts,
12525            )?))
12526        }
12527        "valueOf" => Ok(Value::Float(n)),
12528        _ => Err(host::type_error(&format!("{name} is not a function"))),
12529    }
12530}
12531
12532/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12533/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12534/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12535/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12536///
12537/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12538/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12539/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12540fn to_fixed(n: f64, f: usize) -> String {
12541    if !n.is_finite() {
12542        return host::fmt_number(n);
12543    }
12544    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12545    if n.abs() >= 1e21 {
12546        return host::fmt_number(n);
12547    }
12548    let neg = n < 0.0;
12549    // Exact decimal with guard digits past the rounding position; then round the
12550    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12551    let full = format!("{:.*}", f + 25, n.abs());
12552    let mut body = round_decimal_string(&full, f);
12553    if neg {
12554        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12555    }
12556    body
12557}
12558
12559/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12560/// fractional digits, half away from zero, propagating carry across the point.
12561fn round_decimal_string(s: &str, f: usize) -> String {
12562    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12563    let mut digits: Vec<u8> = int_part
12564        .bytes()
12565        .chain(frac_part.bytes())
12566        .map(|b| b - b'0')
12567        .collect();
12568    let point = int_part.len(); // digits before the decimal point
12569    let keep = point + f; // number of leading digits to keep
12570
12571    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12572    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12573        let mut i = keep;
12574        loop {
12575            if i == 0 {
12576                digits.insert(0, 1);
12577                // A new leading digit shifts the decimal point right by one.
12578                return assemble_decimal(&digits, point + 1, f);
12579            }
12580            i -= 1;
12581            if digits[i] == 9 {
12582                digits[i] = 0;
12583            } else {
12584                digits[i] += 1;
12585                break;
12586            }
12587        }
12588    }
12589    assemble_decimal(&digits, point, f)
12590}
12591
12592/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12593/// `point` digits precede the decimal point.
12594fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12595    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12596    let int_str = int_str.trim_start_matches('0');
12597    let int_str = if int_str.is_empty() { "0" } else { int_str };
12598    if f == 0 {
12599        return int_str.to_string();
12600    }
12601    let frac: String = digits[point..point + f]
12602        .iter()
12603        .map(|d| (d + b'0') as char)
12604        .collect();
12605    format!("{int_str}.{frac}")
12606}
12607
12608/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12609/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12610/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12611/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12612/// exact digits are taken with guard positions and rounded here.
12613fn round_significant(a: f64, p: usize) -> (String, i32) {
12614    let sci = format!("{a:.*e}", p - 1 + 25);
12615    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12616    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12617    let all: Vec<u8> = mant
12618        .chars()
12619        .filter(|c| c.is_ascii_digit())
12620        .map(|c| c as u8 - b'0')
12621        .collect();
12622    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12623    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12624        // Round the p-digit mantissa up, propagating carry; a carry out of the
12625        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12626        let mut d: Vec<u8> = all[..p].to_vec();
12627        let mut i = p;
12628        loop {
12629            if i == 0 {
12630                d.insert(0, 1);
12631                d.truncate(p);
12632                e += 1;
12633                break;
12634            }
12635            i -= 1;
12636            if d[i] == 9 {
12637                d[i] = 0;
12638            } else {
12639                d[i] += 1;
12640                break;
12641            }
12642        }
12643        s = d.iter().map(|x| (x + b'0') as char).collect();
12644    }
12645    (s, e)
12646}
12647
12648/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12649/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12650/// `f` omitted, as many digits as uniquely identify the value are used
12651/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12652/// on the exact value, matching `toPrecision`.
12653fn to_exponential(n: f64, f: Option<usize>) -> String {
12654    if !n.is_finite() {
12655        return host::fmt_number(n);
12656    }
12657    let neg = n < 0.0;
12658    let a = n.abs();
12659    let (s, e) = if a == 0.0 {
12660        // Zero has no significant digits: emit "0" padded to the requested width.
12661        ("0".repeat(f.unwrap_or(0) + 1), 0)
12662    } else {
12663        match f {
12664            Some(f) => round_significant(a, f + 1),
12665            None => {
12666                // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12667                let sci = format!("{a:e}");
12668                let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12669                let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12670                let trimmed = digits.trim_end_matches('0');
12671                let digits = if trimmed.is_empty() { "0" } else { trimmed };
12672                (digits.to_string(), exp_str.parse().unwrap_or(0))
12673            }
12674        }
12675    };
12676    let sign = if e >= 0 { '+' } else { '-' };
12677    let mag = e.abs();
12678    let body = if s.len() == 1 {
12679        format!("{s}e{sign}{mag}")
12680    } else {
12681        format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12682    };
12683    if neg {
12684        format!("-{body}")
12685    } else {
12686        body
12687    }
12688}
12689
12690/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12691/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12692/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12693/// retained (`(100).toPrecision(5) === "100.00"`).
12694fn to_precision(n: f64, p: usize) -> String {
12695    if !n.is_finite() {
12696        return host::fmt_number(n);
12697    }
12698    if n == 0.0 {
12699        return if p == 1 {
12700            "0".into()
12701        } else {
12702            format!("0.{}", "0".repeat(p - 1))
12703        };
12704    }
12705    let neg = n < 0.0;
12706    let (s, e) = round_significant(n.abs(), p);
12707    let pp = p as i32;
12708
12709    let body = if e < -6 || e >= pp {
12710        // Exponential: first digit, optional '.rest', signed exponent.
12711        let sign = if e >= 0 { '+' } else { '-' };
12712        let mag = e.abs();
12713        if p == 1 {
12714            format!("{s}e{sign}{mag}")
12715        } else {
12716            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12717        }
12718    } else if e >= 0 {
12719        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12720        let ip = (e + 1) as usize;
12721        if ip == p {
12722            s
12723        } else {
12724            format!("{}.{}", &s[..ip], &s[ip..])
12725        }
12726    } else {
12727        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12728        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12729    };
12730    if neg {
12731        format!("-{body}")
12732    } else {
12733        body
12734    }
12735}
12736
12737/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12738/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12739/// is emitted exact, and fractional digits are produced up to the input double's
12740/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12741/// carry-over back into already-written digits (and into the integer part).
12742fn to_radix(n: f64, radix: u32) -> String {
12743    if !n.is_finite() {
12744        return host::fmt_number(n);
12745    }
12746    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12747    let rf = radix as f64;
12748    let neg = n < 0.0;
12749    let value = n.abs();
12750
12751    let mut integer = value.floor();
12752    let mut fraction = value - integer;
12753
12754    // Fraction digits, most-significant first.
12755    let mut frac: Vec<u8> = Vec::new();
12756    // Only compute fractional digits down to the input double's precision.
12757    let mut delta = 0.5 * (next_up(value) - value);
12758    delta = delta.max(next_up(0.0));
12759    if fraction >= delta {
12760        loop {
12761            // Shift up by one digit.
12762            fraction *= rf;
12763            delta *= rf;
12764            let digit = fraction as usize;
12765            frac.push(digits[digit]);
12766            fraction -= digit as f64;
12767            // Round to even.
12768            if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12769                // Carry-over: back-trace already-written fraction digits.
12770                loop {
12771                    match frac.pop() {
12772                        None => {
12773                            // Carried past the point into the integer part.
12774                            integer += 1.0;
12775                            break;
12776                        }
12777                        Some(c) => {
12778                            let d = if c > b'9' {
12779                                (c - b'a' + 10) as u32
12780                            } else {
12781                                (c - b'0') as u32
12782                            };
12783                            if d + 1 < radix {
12784                                frac.push(digits[(d + 1) as usize]);
12785                                break;
12786                            }
12787                            // digit was radix-1: drop it and keep carrying.
12788                        }
12789                    }
12790                }
12791                break;
12792            }
12793            if fraction < delta {
12794                break;
12795            }
12796        }
12797    }
12798
12799    // Integer digits, least-significant first (reversed at the end).
12800    let mut int_out: Vec<u8> = Vec::new();
12801    // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12802    while v8_exponent(integer / rf) > 0 {
12803        integer /= rf;
12804        int_out.push(b'0');
12805    }
12806    loop {
12807        let remainder = integer % rf;
12808        int_out.push(digits[remainder as usize]);
12809        integer = (integer - remainder) / rf;
12810        if integer <= 0.0 {
12811            break;
12812        }
12813    }
12814    int_out.reverse();
12815
12816    let mut out: Vec<u8> = Vec::new();
12817    if neg {
12818        out.push(b'-');
12819    }
12820    out.extend_from_slice(&int_out);
12821    if !frac.is_empty() {
12822        out.push(b'.');
12823        out.extend_from_slice(&frac);
12824    }
12825    String::from_utf8(out).unwrap()
12826}
12827
12828/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12829fn next_up(x: f64) -> f64 {
12830    f64::from_bits(x.to_bits() + 1)
12831}
12832
12833/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12834/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12835fn v8_exponent(x: f64) -> i32 {
12836    let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12837    if biased == 0 {
12838        -1074 // denormal
12839    } else {
12840        biased - 1075
12841    }
12842}
12843
12844// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12845
12846/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12847/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12848/// but the value kept alongside it is what iteration and `console.log` report,
12849/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12850fn normalize_zero_key(v: Value) -> Value {
12851    match v {
12852        Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12853        other => other,
12854    }
12855}
12856
12857fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12858    match name {
12859        "get" => {
12860            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12861            Ok(with_host(|h| match h.get(recv) {
12862                Some(JsObj::Map { entries, .. }) => entries
12863                    .get(&key)
12864                    .map(|(_, v)| v.clone())
12865                    .unwrap_or(Value::Undef),
12866                _ => Value::Undef,
12867            }))
12868        }
12869        "set" => {
12870            let kv = normalize_zero_key(arg0(&args));
12871            let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12872            reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12873            let key = with_host(|h| host::map_key(h, &kv));
12874            with_host(|h| {
12875                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12876                    entries.insert(key, (kv, vv));
12877                }
12878            });
12879            Ok(recv.clone())
12880        }
12881        "has" => {
12882            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12883            Ok(Value::Bool(with_host(
12884                |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12885            )))
12886        }
12887        "delete" => {
12888            let key = with_host(|h| host::map_key(h, &arg0(&args)));
12889            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12890                Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12891                _ => false,
12892            })))
12893        }
12894        "clear" => {
12895            with_host(|h| {
12896                if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12897                    entries.clear();
12898                }
12899            });
12900            Ok(Value::Undef)
12901        }
12902        "forEach" => {
12903            let cb = arg0(&args);
12904            let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12905                Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12906                _ => Vec::new(),
12907            });
12908            for (k, v) in pairs {
12909                host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12910            }
12911            Ok(Value::Undef)
12912        }
12913        // LIVE, not a snapshot: an entry added during iteration is visited and
12914        // one deleted before it is reached is not.
12915        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12916            recv,
12917            if name == "@@iterator" {
12918                "entries"
12919            } else {
12920                name
12921            },
12922        )),
12923        _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12924    }
12925}
12926
12927/// A weak collection can only hold objects (and unregistered symbols) — a
12928/// primitive key is a `TypeError`, which is how packages probe for weak support.
12929fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12930    let weak = with_host(|h| {
12931        matches!(
12932            h.get(recv),
12933            Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12934        )
12935    });
12936    if !weak {
12937        return Ok(());
12938    }
12939    let is_object = with_host(|h| match key {
12940        Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12941        _ => false,
12942    });
12943    if is_object {
12944        return Ok(());
12945    }
12946    Err(host::type_error(if kind == "WeakMap" {
12947        "Invalid value used as weak map key"
12948    } else {
12949        "Invalid value used in weak set"
12950    }))
12951}
12952
12953/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12954///
12955/// The seven set operations do NOT require a real `Set` on the right-hand side:
12956/// anything with a numeric `size` and callable `has`/`keys` participates, which
12957/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12958/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12959/// diagnostic, so a bad operand reports which field was wrong rather than
12960/// failing later inside the iteration.
12961struct SetRecord {
12962    obj: Value,
12963    /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12964    /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12965    /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12966    size: f64,
12967    has: Value,
12968    keys: Value,
12969}
12970
12971fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12972    if !with_host(|h| is_object_like(h, other)) {
12973        return Err(host::type_error(&format!(
12974            "Set.prototype.{method} argument must be an object"
12975        )));
12976    }
12977    let raw = get_property(other, "size")?;
12978    let num = host::to_number_value(&raw)?;
12979    if num.is_nan() {
12980        return Err(host::type_error("The .size property is NaN"));
12981    }
12982    let size = num.trunc();
12983    if size < 0.0 {
12984        return Err(host::range_error(&format!("'{size}' is an invalid size")));
12985    }
12986    let has = get_property(other, "has")?;
12987    if !with_host(|h| host::is_callable(h, &has)) {
12988        return Err(host::type_error("string \"has\" is not a function"));
12989    }
12990    let keys = get_property(other, "keys")?;
12991    if !with_host(|h| host::is_callable(h, &keys)) {
12992        return Err(host::type_error("string \"keys\" is not a function"));
12993    }
12994    Ok(SetRecord {
12995        obj: other.clone(),
12996        size,
12997        has,
12998        keys,
12999    })
13000}
13001
13002impl SetRecord {
13003    /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
13004    /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
13005    fn has(&self, v: &Value) -> Result<bool, String> {
13006        let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
13007        Ok(with_host(|h| h.truthy(&r)))
13008    }
13009
13010    /// The operand's elements, drained from the iterator its `keys` method
13011    /// returns. A non-object result is the spec's `Result of the keys method is
13012    /// not an object`, reported before anything is iterated.
13013    fn keys(&self) -> Result<Vec<Value>, String> {
13014        let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
13015        if !with_host(|h| is_object_like(h, &it)) {
13016            return Err(host::type_error(
13017                "Result of the keys method is not an object",
13018            ));
13019        }
13020        host::drain_iterator(&it)
13021    }
13022}
13023
13024/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13025/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13026/// LEFT even though it can on the right.
13027fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13028    if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13029        return Ok(());
13030    }
13031    Err(host::type_error(&format!(
13032        "Method Set.prototype.{method} called on incompatible receiver {}",
13033        with_host(|h| object_tag(h, recv))
13034    )))
13035}
13036
13037/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13038///
13039/// Every one of these operations copies `[[SetData]]` *after* it has touched
13040/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13041/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13042/// result. Snapshotting the receiver up front instead dropped such an element:
13043/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13044fn set_values(recv: &Value) -> Vec<Value> {
13045    with_host(|h| match h.get(recv) {
13046        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13047        _ => Vec::new(),
13048    })
13049}
13050
13051fn set_size(recv: &Value) -> f64 {
13052    with_host(|h| match h.get(recv) {
13053        Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13054        _ => 0.0,
13055    })
13056}
13057
13058/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13059/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13060fn new_set(items: Vec<Value>) -> Result<Value, String> {
13061    let s = with_host(|h| {
13062        h.alloc(JsObj::Set {
13063            entries: IndexMap::new(),
13064            weak: false,
13065        })
13066    });
13067    for v in items {
13068        set_method(&s, "add", vec![v])?;
13069    }
13070    Ok(s)
13071}
13072
13073fn set_contains(s: &Value, v: &Value) -> bool {
13074    let key = with_host(|h| host::map_key(h, v));
13075    with_host(
13076        |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13077    )
13078}
13079
13080/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13081///
13082/// Each one branches on the two sizes and iterates the SMALLER side — not an
13083/// optimization but observable behaviour: which side is walked decides the
13084/// result's order and whether the operand's `has` or its `keys` is the method
13085/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13086/// yields the operand's order, and its `keys` (never its `has`) is called.
13087fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13088    require_set_receiver(recv, name)?;
13089    let other = get_set_record(&arg0(&args), name)?;
13090    let my_size = set_size(recv);
13091    match name {
13092        "union" => {
13093            let keys = other.keys()?;
13094            let mut out = set_values(recv);
13095            out.extend(keys);
13096            new_set(out)
13097        }
13098        "intersection" => {
13099            let mut out = Vec::new();
13100            if my_size <= other.size {
13101                for v in set_values(recv) {
13102                    if other.has(&v)? {
13103                        out.push(v);
13104                    }
13105                }
13106            } else {
13107                for k in other.keys()? {
13108                    if set_contains(recv, &k) {
13109                        out.push(k);
13110                    }
13111                }
13112            }
13113            new_set(out)
13114        }
13115        "difference" => {
13116            if my_size <= other.size {
13117                let mut out = Vec::new();
13118                for v in set_values(recv) {
13119                    if !other.has(&v)? {
13120                        out.push(v);
13121                    }
13122                }
13123                return new_set(out);
13124            }
13125            let out = new_set(set_values(recv))?;
13126            for k in other.keys()? {
13127                set_method(&out, "delete", vec![k])?;
13128            }
13129            Ok(out)
13130        }
13131        "symmetricDifference" => {
13132            // The operand is drained FIRST — the spec takes the iterator before
13133            // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13134            // reflected in the result.
13135            let keys = other.keys()?;
13136            let out = new_set(set_values(recv))?;
13137            for k in keys {
13138                if set_contains(recv, &k) {
13139                    set_method(&out, "delete", vec![k])?;
13140                } else {
13141                    set_method(&out, "add", vec![k])?;
13142                }
13143            }
13144            Ok(out)
13145        }
13146        "isSubsetOf" => {
13147            if my_size > other.size {
13148                return Ok(Value::Bool(false));
13149            }
13150            for v in set_values(recv) {
13151                if !other.has(&v)? {
13152                    return Ok(Value::Bool(false));
13153                }
13154            }
13155            Ok(Value::Bool(true))
13156        }
13157        "isSupersetOf" => {
13158            if my_size < other.size {
13159                return Ok(Value::Bool(false));
13160            }
13161            for k in other.keys()? {
13162                if !set_contains(recv, &k) {
13163                    return Ok(Value::Bool(false));
13164                }
13165            }
13166            Ok(Value::Bool(true))
13167        }
13168        "isDisjointFrom" => {
13169            if my_size <= other.size {
13170                for v in set_values(recv) {
13171                    if other.has(&v)? {
13172                        return Ok(Value::Bool(false));
13173                    }
13174                }
13175            } else {
13176                for k in other.keys()? {
13177                    if set_contains(recv, &k) {
13178                        return Ok(Value::Bool(false));
13179                    }
13180                }
13181            }
13182            Ok(Value::Bool(true))
13183        }
13184        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13185    }
13186}
13187
13188fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13189    match name {
13190        "add" => {
13191            let vv = normalize_zero_key(arg0(&args));
13192            reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13193            let key = with_host(|h| host::map_key(h, &vv));
13194            with_host(|h| {
13195                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13196                    entries.insert(key, vv);
13197                }
13198            });
13199            Ok(recv.clone())
13200        }
13201        "has" => {
13202            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13203            Ok(Value::Bool(with_host(
13204                |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13205            )))
13206        }
13207        "delete" => {
13208            let key = with_host(|h| host::map_key(h, &arg0(&args)));
13209            Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13210                Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13211                _ => false,
13212            })))
13213        }
13214        "clear" => {
13215            with_host(|h| {
13216                if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13217                    entries.clear();
13218                }
13219            });
13220            Ok(Value::Undef)
13221        }
13222        "forEach" => {
13223            let cb = arg0(&args);
13224            let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13225                Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13226                _ => Vec::new(),
13227            });
13228            for v in vals {
13229                host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13230            }
13231            Ok(Value::Undef)
13232        }
13233        "union"
13234        | "intersection"
13235        | "difference"
13236        | "symmetricDifference"
13237        | "isSubsetOf"
13238        | "isSupersetOf"
13239        | "isDisjointFrom" => set_operation(recv, name, args),
13240        // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13241        "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13242            recv,
13243            if name == "entries" {
13244                "entries"
13245            } else {
13246                "values"
13247            },
13248        )),
13249        _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13250    }
13251}
13252
13253fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13254    // A generator IS its own iterator: both symbol forms return the receiver.
13255    if matches!(name, "@@iterator" | "@@asyncIterator") {
13256        return Ok(recv.clone());
13257    }
13258    // An `async function*` object's methods return PROMISES of the record, and
13259    // its body has to be driven through the await-aware stepper (a plain
13260    // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13261    if host::is_async_generator(recv) {
13262        // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13263        // `.return`/`.throw` must wait behind a `.next()` that is still
13264        // suspended on an internal `await`, or that `.next()` would report
13265        // `{done: true}` for a value the body had not yet reached. An uncaught
13266        // `.throw(e)` rejects the returned promise; it does not throw here.
13267        return match name {
13268            "next" => Ok(host::async_gen_enqueue(
13269                recv,
13270                host::GenReq::Next(arg0(&args)),
13271            )),
13272            "return" => Ok(host::async_gen_enqueue(
13273                recv,
13274                host::GenReq::Return(arg0(&args)),
13275            )),
13276            "throw" => Ok(host::async_gen_enqueue(
13277                recv,
13278                host::GenReq::Throw(arg0(&args)),
13279            )),
13280            "@@asyncIterator" => Ok(recv.clone()),
13281            _ => Err(host::type_error(&format!(
13282                "asyncGenerator.{name} is not a function"
13283            ))),
13284        };
13285    }
13286    match name {
13287        "next" => {
13288            let send = arg0(&args);
13289            match host::gen_resume(recv, send)? {
13290                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13291                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13292            }
13293        }
13294        "return" => {
13295            // Resume with an injected return so any pending `finally` runs; the
13296            // completion may itself be a `finally` yield (not-done) or the value.
13297            match host::gen_return(recv, arg0(&args))? {
13298                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13299                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13300            }
13301        }
13302        "throw" => {
13303            // Inject a throw at the suspension point: an enclosing `try/catch` in
13304            // the body can handle it (and any `finally` runs); otherwise it
13305            // propagates to the caller.
13306            match host::gen_throw(recv, arg0(&args))? {
13307                host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13308                host::GenStep::Done(v) => Ok(iter_result(v, true)),
13309            }
13310        }
13311        _ => Err(host::type_error(&format!(
13312            "generator.{name} is not a function"
13313        ))),
13314    }
13315}
13316
13317/// A `{ value, done }` iterator-result object.
13318fn iter_result(value: Value, done: bool) -> Value {
13319    with_host(|h| {
13320        let mut m: IndexMap<String, Value> = IndexMap::new();
13321        m.insert("value".into(), value);
13322        m.insert("done".into(), Value::Bool(done));
13323        h.new_object(m)
13324    })
13325}
13326
13327/// A live iterator over array `arr` — what `keys()`, `values()`, `entries()`
13328/// and `Symbol.iterator` return, and what a `for-of` over an array steps.
13329pub(crate) fn array_iterator(arr: &Value, kind: host::ArrayIterKind) -> Value {
13330    with_host(|h| {
13331        h.alloc(JsObj::Iter {
13332            items: Vec::new(),
13333            idx: 0,
13334            array: Some((arr.clone(), kind)),
13335        })
13336    })
13337}
13338
13339/// One step of a `JsObj::Iter`: `None` when `it` is not one, `Some(None)` once
13340/// it is exhausted, otherwise the next value.
13341///
13342/// An array iterator reads the array at every step (23.1.5.1
13343/// `%ArrayIteratorPrototype%.next`): the length is re-read, so an element
13344/// pushed during a `for-of` is visited and one popped is not, and the element
13345/// is read as `a[i]` reads it — an accessor runs, a hole reads through the
13346/// prototype. Once it reports done it stays done, even if the array grows.
13347pub(crate) fn iter_step(it: &Value) -> Option<Option<Value>> {
13348    use host::ArrayIterKind;
13349    // One host borrow for the common case — a snapshot, or an array slot that
13350    // is neither a hole nor an accessor. `Err` carries what only `[[Get]]` can
13351    // read: the array, the kind and the index.
13352    let step = with_host(|h| {
13353        let (arr, kind, i) = match h.get_mut(it) {
13354            Some(JsObj::Iter {
13355                items,
13356                idx,
13357                array: None,
13358            }) => {
13359                let v = items.get(*idx).cloned();
13360                if v.is_some() {
13361                    *idx += 1;
13362                }
13363                return Some(Ok(v));
13364            }
13365            Some(JsObj::Iter {
13366                idx,
13367                array: Some((arr, kind)),
13368                ..
13369            }) => (arr.clone(), *kind, *idx),
13370            _ => return None,
13371        };
13372        // `usize::MAX` marks an iterator that has already reported done, and
13373        // it stays done even if the array grows.
13374        let len = match h.get(&arr) {
13375            Some(JsObj::Array(items)) => items.len(),
13376            _ => 0,
13377        };
13378        let done = i == usize::MAX || i >= len;
13379        if let Some(JsObj::Iter { idx, .. }) = h.get_mut(it) {
13380            *idx = if done { usize::MAX } else { i + 1 };
13381        }
13382        if done {
13383            return Some(Ok(None));
13384        }
13385        let key = Value::Float(i as f64);
13386        let slot = match (kind, h.get(&arr)) {
13387            (ArrayIterKind::Keys, _) => return Some(Ok(Some(key))),
13388            (_, Some(JsObj::Array(items)))
13389                if !h.is_hole(&arr, i) && h.own_accessor_keys(&arr).is_empty() =>
13390            {
13391                items[i].clone()
13392            }
13393            _ => return Some(Err((arr, kind, i))),
13394        };
13395        Some(Ok(Some(match kind {
13396            ArrayIterKind::Entries => h.new_array(vec![key, slot]),
13397            _ => slot,
13398        })))
13399    })?;
13400    let (arr, kind, i) = match step {
13401        Ok(step) => return Some(step),
13402        Err(slow) => slow,
13403    };
13404    let value = get_property(&arr, &i.to_string()).unwrap_or(Value::Undef);
13405    Some(Some(match kind {
13406        ArrayIterKind::Entries => with_host(|h| h.new_array(vec![Value::Float(i as f64), value])),
13407        _ => value,
13408    }))
13409}
13410
13411/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a
13412/// cursor over a snapshot, or live over an array ([`iter_step`]).
13413fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13414    match name {
13415        "next" => Ok(match iter_step(recv).flatten() {
13416            Some(v) => iter_result(v, false),
13417            None => iter_result(Value::Undef, true),
13418        }),
13419        "return" => {
13420            // Exhaust the cursor and report done.
13421            with_host(|h| {
13422                if let Some(JsObj::Iter { items, idx, array }) = h.get_mut(recv) {
13423                    *idx = if array.is_some() {
13424                        usize::MAX
13425                    } else {
13426                        items.len()
13427                    };
13428                }
13429            });
13430            Ok(iter_result(arg0(&args), true))
13431        }
13432        // An iterator is its own iterable.
13433        "@@iterator" => Ok(recv.clone()),
13434        _ => Err(host::type_error(&format!(
13435            "iterator.{name} is not a function"
13436        ))),
13437    }
13438}
13439
13440fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13441    match name {
13442        "toString" => Ok(with_host(|h| {
13443            let s = h.str_of(recv);
13444            h.new_str(s)
13445        })),
13446        // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13447        // itself for EVERY hint — it ignores its argument. That is what makes
13448        // `sym + ''` a TypeError rather than a concatenation: the conversion
13449        // succeeds and hands back a symbol, and it is `+` that then rejects it.
13450        "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13451        _ => Err(host::type_error(&format!(
13452            "symbol.{name} is not a function"
13453        ))),
13454    }
13455}
13456
13457// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13458
13459fn object_create(args: Vec<Value>) -> Result<Value, String> {
13460    let proto = arg0(&args);
13461    // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13462    // `undefined` is NOT accepted — measured on node v26.7.0,
13463    // `Object.create(undefined)` is
13464    // `TypeError: Object prototype may only be an Object or null: undefined`,
13465    // where node-js quietly built a normal object.
13466    reject_bad_prototype(&proto)?;
13467    let obj = with_host(|h| h.new_object(IndexMap::new()));
13468    // `set_proto` records a null proto as an explicit null-prototype object.
13469    with_host(|h| h.set_proto(&obj, proto));
13470    // Optional second arg: a property-descriptor map.
13471    if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13472        let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13473            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13474            _ => Vec::new(),
13475        });
13476        for (k, d) in entries {
13477            apply_descriptor(&obj, &k, &d)?;
13478        }
13479    }
13480    Ok(obj)
13481}
13482
13483/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13484/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13485/// `EventEmitter.prototype` (the one express mixes onto its app function).
13486/// The own property names of `<Ctor>.prototype`, and whether each is
13487/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13488///
13489/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13490/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13491/// but were not enumerable, so feature detection that walks a prototype found
13492/// nothing there. The table is read from the reference engine rather than
13493/// derived from the arity table because the arity table holds functions only:
13494/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13495/// `URL.prototype` components are accessors.
13496fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13497    let ctor = ns.strip_suffix(".prototype")?;
13498    crate::arity::PROTO_MEMBERS
13499        .binary_search_by(|(k, _)| (*k).cmp(ctor))
13500        .ok()
13501        .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13502}
13503
13504fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13505    match ns {
13506        "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13507        _ => None,
13508    }
13509}
13510
13511/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13512/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13513/// filtered out of); every other receiver answers from its property map.
13514fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13515    if let Some(keys) = crate::proxy::own_keys(v)? {
13516        return Ok(keys
13517            .iter()
13518            .filter(|k| host::is_symbol_key(k))
13519            .map(|k| crate::proxy::key_value(k))
13520            .collect());
13521    }
13522    // An intrinsic prototype's symbol-keyed members come from the generated
13523    // table, which is the only record of them: they own no map entry, so
13524    // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13525    // reports `Symbol.iterator` and `Symbol.unscopables`.
13526    if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13527        if let Some(members) = intrinsic_proto_members(&ns) {
13528            return Ok(with_host(|h| {
13529                members
13530                    .iter()
13531                    .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13532                    .map(|name| h.well_known_symbol(name))
13533                    .collect()
13534            }));
13535        }
13536    }
13537    Ok(with_host(|h| h.own_symbol_keys(v)))
13538}
13539
13540/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13541pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13542    object_define_property(vec![obj.clone(), key, desc])
13543}
13544
13545/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13546pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13547    object_get_own_descriptor(vec![obj.clone(), key])
13548}
13549
13550fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13551    let obj = arg0(&args);
13552    // A Proxy defines through its `defineProperty` trap; the target it forwards
13553    // to is where the ordinary path below finally runs.
13554    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13555        let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13556        let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13557        if !with_host(|h| is_object_like(h, &desc)) {
13558            return Err(host::type_error(&format!(
13559                "Property description must be an object: {}",
13560                with_host(|h| h.str_of(&desc))
13561            )));
13562        }
13563        // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13564        // too. `Reflect.defineProperty` is the form that reports `false`.
13565        if !crate::proxy::define_property(&obj, &key, &desc)? {
13566            return Err(host::type_error(&format!(
13567                "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13568            )));
13569        }
13570        return Ok(obj);
13571    }
13572    // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13573    // target and a non-object descriptor each throw before anything is written.
13574    if !with_host(|h| is_object_like(h, &obj)) {
13575        return Err(host::type_error(
13576            "Object.defineProperty called on non-object",
13577        ));
13578    }
13579    let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13580    if !with_host(|h| is_object_like(h, &desc)) {
13581        return Err(host::type_error(&format!(
13582            "Property description must be an object: {}",
13583            with_host(|h| h.str_of(&desc))
13584        )));
13585    }
13586    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13587    apply_descriptor(&obj, &key, &desc)?;
13588    Ok(obj)
13589}
13590
13591/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13592/// for anything else (28.1). A primitive was being accepted and silently
13593/// producing nothing.
13594/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13595/// `Reflect.construct` take.
13596///
13597/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13598/// iterator was being used instead, so an array-like produced nothing and a
13599/// primitive produced nothing rather than the TypeError node raises.
13600/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13601/// object still accepts, because it changes nothing.
13602///
13603/// The observable prototype, not the stored link: an ordinary object has no
13604/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13605/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13606/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13607/// step 8 walks up from `p` looking for `obj`.
13608///
13609/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13610/// built a ring. Nothing hung, because every chain walk in this host carries a
13611/// hop limit, but a lookup then silently gave up instead of finding a property
13612/// that really was there.
13613fn would_cycle(obj: &Value, p: &Value) -> bool {
13614    let mut cur = Some(p.clone());
13615    for _ in 0..1000 {
13616        let Some(c) = cur else { return false };
13617        if with_host(|h| h.strict_eq(&c, obj)) {
13618            return true;
13619        }
13620        // A PROXY's prototype is its handler's business; the spec skips the
13621        // walk entirely when one is in the chain.
13622        if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13623            return false;
13624        }
13625        cur = with_host(|h| h.proto_of(&c));
13626    }
13627    false
13628}
13629
13630fn same_prototype(obj: &Value, p: &Value) -> bool {
13631    let cur = prototype_of(obj);
13632    with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13633}
13634
13635fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13636    if !with_host(|h| is_object_like(h, v)) {
13637        return Err(host::type_error(
13638            "CreateListFromArrayLike called on non-object",
13639        ));
13640    }
13641    let len = get_property(v, "length")?;
13642    let n = with_host(|h| h.to_number(&len));
13643    let n = if n.is_finite() && n > 0.0 {
13644        n as usize
13645    } else {
13646        0
13647    };
13648    (0..n).map(|i| get_property(v, &i.to_string())).collect()
13649}
13650
13651fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13652    if with_host(|h| is_object_like(h, v)) {
13653        return Ok(());
13654    }
13655    Err(host::type_error(&format!(
13656        "Reflect.{method} called on non-object"
13657    )))
13658}
13659
13660/// Whether `v` is an Object in the language sense — anything `typeof` calls
13661/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13662/// distinguish "an object" from a primitive.
13663fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13664    matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13665}
13666
13667/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13668/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13669/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13670/// `assign` family: each returned an empty result for `null` where node v26.7.0
13671/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13672/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13673fn require_object_coercible(v: &Value) -> Result<(), String> {
13674    if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13675        return Err(host::type_error(
13676            "Cannot convert undefined or null to object",
13677        ));
13678    }
13679    Ok(())
13680}
13681
13682/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13683/// nor `null`, with V8's wording. Measured on node v26.7.0:
13684/// `Object.create("s")` is
13685/// `TypeError: Object prototype may only be an Object or null: s`.
13686fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13687    if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13688        return Ok(());
13689    }
13690    Err(host::type_error(&format!(
13691        "Object prototype may only be an Object or null: {}",
13692        with_host(|h| h.str_of(proto))
13693    )))
13694}
13695
13696/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13697///
13698/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13699/// `configurable` field defaults to **false** — which is why a `defineProperty`
13700/// data property is invisible to `Object.keys` unless the caller opts in. That
13701/// asymmetry against plain assignment is the whole reason the attribute table
13702/// exists.
13703/// The requested fields of a property descriptor — 10.1.6.2
13704/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13705/// is the distinction the merge below turns on: an omitted field LEAVES an
13706/// existing attribute alone rather than resetting it.
13707struct Requested {
13708    value: Option<Value>,
13709    get: Option<Option<Value>>,
13710    set: Option<Option<Value>>,
13711    writable: Option<bool>,
13712    enumerable: Option<bool>,
13713    configurable: Option<bool>,
13714}
13715
13716impl Requested {
13717    /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13718    /// `HasProperty`/`Get` pairs do — a descriptor built with
13719    /// `Object.create({ value: 1 })` is legal.
13720    fn read(desc: &Value) -> Self {
13721        let has = |k: &str| {
13722            with_host(|h| {
13723                host::lookup_chain(h, desc, k).is_some()
13724                    || host::lookup_accessor(h, desc, k).is_some()
13725            })
13726        };
13727        let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13728        // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13729        // doing it inside the `with_host` closure aborts on the double borrow.
13730        let flag = |k: &str| {
13731            has(k).then(|| {
13732                let v = val(k);
13733                with_host(|h| h.truthy(&v))
13734            })
13735        };
13736        Requested {
13737            value: has("value").then(|| val("value")),
13738            get: has("get").then(|| match val("get") {
13739                Value::Undef => None,
13740                g => Some(g),
13741            }),
13742            set: has("set").then(|| match val("set") {
13743                Value::Undef => None,
13744                st => Some(st),
13745            }),
13746            writable: flag("writable"),
13747            enumerable: flag("enumerable"),
13748            configurable: flag("configurable"),
13749        }
13750    }
13751
13752    fn is_accessor(&self) -> bool {
13753        self.get.is_some() || self.set.is_some()
13754    }
13755
13756    fn is_data(&self) -> bool {
13757        self.value.is_some() || self.writable.is_some()
13758    }
13759}
13760
13761/// The own property already at `key`, if any, read back through
13762/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13763/// fn-prop side table, Buffer bytes) is covered by one code path.
13764struct Existing {
13765    accessor: bool,
13766    value: Value,
13767    get: Option<Value>,
13768    set: Option<Value>,
13769    writable: bool,
13770    enumerable: bool,
13771    configurable: bool,
13772}
13773
13774fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13775    let k = with_host(|h| h.new_str(key.to_string()));
13776    let d = own_descriptor_pub(obj, k).ok()?;
13777    if matches!(d, Value::Undef) {
13778        return None;
13779    }
13780    let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13781    let truthy = |n: &str| {
13782        let v = field(n);
13783        with_host(|h| h.truthy(&v))
13784    };
13785    let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13786    Some(Existing {
13787        accessor,
13788        value: field("value"),
13789        get: match field("get") {
13790            Value::Undef => None,
13791            g => Some(g),
13792        },
13793        set: match field("set") {
13794            Value::Undef => None,
13795            st => Some(st),
13796        },
13797        writable: truthy("writable"),
13798        enumerable: truthy("enumerable"),
13799        configurable: truthy("configurable"),
13800    })
13801}
13802
13803/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13804/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13805/// one with this, not with strict equality.
13806pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13807    let num = |v: &Value| match v {
13808        Value::Int(n) => Some(*n as f64),
13809        Value::Float(f) => Some(*f),
13810        _ => None,
13811    };
13812    match (num(a), num(b)) {
13813        (Some(x), Some(y)) => {
13814            if x.is_nan() && y.is_nan() {
13815                true
13816            } else if x == 0.0 && y == 0.0 {
13817                x.is_sign_negative() == y.is_sign_negative()
13818            } else {
13819                x == y
13820            }
13821        }
13822        _ => with_host(|h| h.strict_eq(a, b)),
13823    }
13824}
13825
13826/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13827///
13828/// None of the validation existed: every `Object.defineProperty` was applied
13829/// unconditionally, so redefining a non-configurable property silently
13830/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13831/// `false` rather than "leave alone", so the ordinary
13832/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13833/// `writable` and `configurable` from a property that had both.
13834///
13835/// Converting an accessor to a data property did not take effect at all: the
13836/// value was written but the accessor stayed in its side table, and accessors
13837/// win on read, so the getter kept answering.
13838fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13839    let req = Requested::read(desc);
13840    let cur = existing_property(obj, key);
13841
13842    // An array's `length` is the exotic own property whose write resizes the
13843    // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13844    // key and left the elements untouched.
13845    if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13846        if let Some(v) = req.value.clone() {
13847            return set_property_pub(obj, "length", v);
13848        }
13849    }
13850
13851    // The other exotics whose own properties are SYNTHESIZED rather than stored
13852    // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13853    // The ordinary path below writes a shadowing map entry the read never
13854    // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13855    // unchanged.
13856    // A builtin namespace/prototype has no property map either, so a data
13857    // descriptor has to reach the same side table an assignment does.
13858    // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13859    // careful polyfill installs itself, precisely to avoid the enumerable
13860    // property a bare assignment creates — wrote a map entry nothing read.
13861    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13862        if let Some(v) = req.value.clone() {
13863            return set_property_pub(obj, key, v);
13864        }
13865    }
13866    let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13867        && key.parse::<usize>().is_ok())
13868        || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13869    if exotic_own {
13870        if let Some(v) = req.value.clone() {
13871            return set_property_pub(obj, key, v);
13872        }
13873    }
13874
13875    // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13876    // object. Only an existing property's attributes were being validated, so
13877    // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13878    if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13879        return Err(host::type_error(&format!(
13880            "Cannot define property {key}, object is not extensible"
13881        )));
13882    }
13883    if let Some(c) = &cur {
13884        if !c.configurable {
13885            let rejected = req.configurable == Some(true)
13886                || req.enumerable.is_some_and(|e| e != c.enumerable)
13887                || (req.is_accessor() && !c.accessor)
13888                || (req.is_data() && c.accessor)
13889                || (c.accessor
13890                    && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13891                        || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13892                || (!c.accessor
13893                    && !c.writable
13894                    && (req.writable == Some(true)
13895                        || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13896            if rejected {
13897                return Err(host::type_error(&format!(
13898                    "Cannot redefine property: {key}"
13899                )));
13900            }
13901        }
13902    }
13903
13904    // An omitted field keeps what the property already had; a brand-new
13905    // property defaults every one of them to false.
13906    let attrs = host::PropAttrs {
13907        writable: req
13908            .writable
13909            .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13910        enumerable: req
13911            .enumerable
13912            .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13913        configurable: req
13914            .configurable
13915            .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13916    };
13917    with_host(|h| h.set_prop_attrs(obj, key, attrs));
13918
13919    if req.is_accessor() {
13920        let get = req
13921            .get
13922            .clone()
13923            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13924        let set = req
13925            .set
13926            .clone()
13927            .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13928        // An ACCESSOR at an index past the end still extends the array
13929        // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13930        // `length === 5` with holes between. Only the DATA path grew it, so
13931        // the accessor landed in the side table while `length` stayed put —
13932        // and with it out of range, `Object.keys` and `JSON.stringify` never
13933        // saw the index at all.
13934        if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13935        {
13936            with_host(|h| {
13937                let old_len = match h.get(obj) {
13938                    Some(JsObj::Array(items)) => items.len(),
13939                    _ => 0,
13940                };
13941                if i >= old_len {
13942                    if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13943                        items.resize(i + 1, Value::Undef);
13944                    }
13945                    h.mark_hole_range(obj, old_len..i + 1);
13946                }
13947            });
13948        }
13949        with_host(|h| h.set_accessor(obj, key, get, set));
13950        return Ok(());
13951    }
13952
13953    if let Some(c) = &cur {
13954        if c.accessor {
13955            if !req.is_data() {
13956                // A generic descriptor — flags only — leaves an accessor an
13957                // accessor. They were already applied above.
13958                return Ok(());
13959            }
13960            let v = req.value.clone().unwrap_or(Value::Undef);
13961            with_host(|h| h.accessor_to_data(obj, key, v));
13962            return Ok(());
13963        }
13964    }
13965
13966    let Some(v) = req.value else {
13967        // Nothing to write: a flags-only redefinition of a data property.
13968        return Ok(());
13969    };
13970    write_data_slot(obj, key, v);
13971    Ok(())
13972}
13973
13974/// Store `v` as an own data property, in whichever slot the object kind keeps
13975/// its own properties.
13976fn write_data_slot(obj: &Value, key: &str, v: Value) {
13977    // A function/class receiver stores its own props in the fn-prop side table
13978    // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13979    if matches!(
13980        with_host(|h| h.get(obj).cloned()),
13981        Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13982    ) || uses_side_table(obj)
13983    {
13984        with_host(|h| h.set_fn_prop(obj, key, v));
13985        return;
13986    }
13987    if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13988        // An array's index keys ARE its elements, and defining one past the end
13989        // grows the array with holes in between (10.4.2.1). This whole branch
13990        // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13991        // into the ordinary property map an array does not have, so it was a
13992        // silent no-op.
13993        with_host(|h| {
13994            let old = match h.get(obj) {
13995                Some(JsObj::Array(items)) => items.len(),
13996                _ => 0,
13997            };
13998            if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13999                if i >= old {
14000                    items.resize(i + 1, Value::Undef);
14001                }
14002                items[i] = v;
14003            }
14004            if i > old {
14005                h.mark_hole_range(obj, old..i);
14006            }
14007            h.clear_hole(obj, i);
14008        });
14009        return;
14010    }
14011    with_host(|h| {
14012        if let Some(JsObj::Object(p)) = h.get_mut(obj) {
14013            p.insert(key.to_string(), v);
14014            host::canonicalize_own_keys(p);
14015        }
14016    });
14017}
14018
14019/// `Object.defineProperties(obj, descriptorMap)`.
14020fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
14021    let obj = arg0(&args);
14022    let descs = args.get(1).cloned().unwrap_or(Value::Undef);
14023    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
14024        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
14025        _ => Vec::new(),
14026    });
14027    for (k, d) in entries {
14028        apply_descriptor(&obj, &k, &d)?;
14029    }
14030    Ok(obj)
14031}
14032
14033/// The descriptor of an own property a function, a typed array or a RegExp
14034/// SYNTHESIZES rather than keeping in a property map.
14035///
14036/// These read back through the ordinary path but owned no descriptor and did
14037/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
14038/// views of "does this property exist" disagreed — a read said yes while
14039/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
14040/// what a shim checks before patching.
14041fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
14042    let ro_configurable = host::PropAttrs {
14043        writable: false,
14044        enumerable: false,
14045        configurable: true,
14046    };
14047    // A callable's `length`/`name` are read-only but configurable; its
14048    // `prototype` is writable and NOT configurable, and a class's is neither.
14049    // An arrow, a method and a bound function own no `prototype` at all.
14050    if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
14051        return None;
14052    }
14053    if with_host(|h| host::is_callable(h, obj)) {
14054        if key == "prototype" {
14055            let p = get_property(obj, "prototype").ok()?;
14056            if matches!(p, Value::Undef) {
14057                return None;
14058            }
14059            return Some((
14060                p,
14061                host::PropAttrs {
14062                    writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
14063                    enumerable: false,
14064                    configurable: false,
14065                },
14066            ));
14067        }
14068        return Some((get_property(obj, key).ok()?, ro_configurable));
14069    }
14070    // A typed array's elements are own, enumerable, writable, configurable
14071    // properties; an index past the end owns nothing.
14072    if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
14073        let v = crate::stdlib::typedarray::elem_get(obj, key)?;
14074        return Some((
14075            v,
14076            host::PropAttrs {
14077                writable: true,
14078                enumerable: true,
14079                configurable: true,
14080            },
14081        ));
14082    }
14083    // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
14084    if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
14085        return Some((
14086            get_property(obj, "lastIndex").ok()?,
14087            host::PropAttrs {
14088                writable: true,
14089                enumerable: false,
14090                configurable: false,
14091            },
14092        ));
14093    }
14094    None
14095}
14096
14097fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14098    let obj = arg0(&args);
14099    require_object_coercible(&obj)?;
14100    let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14101    // A string primitive's boxed own properties: each code-unit index is an
14102    // enumerable, non-writable, non-configurable data property, and `length` is
14103    // the same minus enumerable.
14104    if let Some(units) = string_primitive_units(&obj) {
14105        let entry = match key.parse::<usize>() {
14106            Ok(i) => units
14107                .get(i)
14108                .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14109            Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14110            Err(_) => None,
14111        };
14112        return Ok(match entry {
14113            Some((value, enumerable)) => with_host(|h| {
14114                let mut m: IndexMap<String, Value> = IndexMap::new();
14115                m.insert("value".into(), value);
14116                m.insert("writable".into(), Value::Bool(false));
14117                m.insert("enumerable".into(), Value::Bool(enumerable));
14118                m.insert("configurable".into(), Value::Bool(false));
14119                h.new_object(m)
14120            }),
14121            None => Value::Undef,
14122        });
14123    }
14124    if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14125        return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14126    }
14127    // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14128    // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14129    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14130        if let Some(names) = builtin_proto_method_names(&ns) {
14131            if names.contains(&key.as_str()) {
14132                return Ok(with_host(|h| {
14133                    let thunk = h.alloc(JsObj::Builtin(format!(
14134                        "@proto:{}:{key}",
14135                        ns.trim_end_matches(".prototype")
14136                    )));
14137                    let mut m: IndexMap<String, Value> = IndexMap::new();
14138                    m.insert("value".into(), thunk);
14139                    m.insert("writable".into(), Value::Bool(true));
14140                    m.insert("enumerable".into(), Value::Bool(true));
14141                    m.insert("configurable".into(), Value::Bool(true));
14142                    h.new_object(m)
14143                }));
14144            }
14145        }
14146    }
14147    // A global the object does not own outright is still an own property of the
14148    // global object — the same lazy binding the bare identifier resolves to.
14149    // Every one of them reported `undefined`, so a feature probe written as
14150    // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14151    // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14152    // rest are ordinary writable, non-enumerable, configurable bindings.
14153    if with_host(|h| h.is_global_object(&obj)) {
14154        let owned = with_host(|h| match h.get(&obj) {
14155            Some(JsObj::Object(p)) => p.contains_key(&key),
14156            _ => false,
14157        });
14158        if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14159            // A global a SCRIPT created — `x = 1` with no declaration — is an
14160            // ordinary enumerable property, unlike the builtins.
14161            let script_made = with_host(|h| h.read_global(&key).is_some());
14162            if let Some(v) = global_object_binding(&key) {
14163                let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14164                return Ok(with_host(|h| {
14165                    let mut m: IndexMap<String, Value> = IndexMap::new();
14166                    m.insert("value".into(), v);
14167                    m.insert("writable".into(), Value::Bool(!frozen));
14168                    m.insert(
14169                        "enumerable".into(),
14170                        Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14171                    );
14172                    m.insert("configurable".into(), Value::Bool(!frozen));
14173                    h.new_object(m)
14174                }));
14175            }
14176        }
14177    }
14178    // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14179    // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14180    // one of these reads back a value, but none owned a DESCRIPTOR:
14181    // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14182    // as "no such property" to the shim/polyfill family that probes a namespace
14183    // before patching it.
14184    // An ACCESSOR member describes itself with a `get`, never a `value` — and
14185    // it must do so without READING the property, since running the getter
14186    // against the prototype is exactly what throws. Both prototype
14187    // representations are covered, so `Symbol.prototype.description` and
14188    // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14189    // "no such property" to anything that probes before patching.
14190    if let Some(ctor) = intrinsic_proto_of(&obj) {
14191        if is_proto_accessor(&ctor, &key) {
14192            let getter = proto_getter(&ctor, &key);
14193            // The poison pair is the only ECMAScript accessor here with a
14194            // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14195            // `hostname` and the rest are all writable, and reporting them as
14196            // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14197            // 'href').set` read `undefined` for a setter that runs.
14198            let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14199                || crate::stdlib::instance_accessors(&ctor)
14200                    .0
14201                    .iter()
14202                    .any(|(k, settable)| *k == key && *settable);
14203            let setter = writable
14204                .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14205            return Ok(with_host(|h| {
14206                let mut m: IndexMap<String, Value> = IndexMap::new();
14207                m.insert("get".into(), getter);
14208                // `undefined`, not null: a read-only accessor has no setter at
14209                // all, and `JSON.stringify` of the descriptor must drop the key
14210                // rather than report `"set": null`.
14211                m.insert("set".into(), setter.unwrap_or(Value::Undef));
14212                m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14213                m.insert("configurable".into(), Value::Bool(true));
14214                h.new_object(m)
14215            }));
14216        }
14217    }
14218    if let Some(ns) = with_host(|h| match h.get(&obj) {
14219        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14220        _ => None,
14221    }) {
14222        let value = namespace_property(&ns, &key);
14223        if !matches!(value, Value::Undef) {
14224            return Ok(builtin_member_descriptor(&ns, &key, value));
14225        }
14226    }
14227    if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14228        return Ok(with_host(|h| {
14229            let mut m: IndexMap<String, Value> = IndexMap::new();
14230            m.insert("value".into(), value);
14231            m.insert("writable".into(), Value::Bool(attrs.writable));
14232            m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14233            m.insert("configurable".into(), Value::Bool(attrs.configurable));
14234            h.new_object(m)
14235        }));
14236    }
14237    // Accessor descriptor?
14238    if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14239        return Ok(with_host(|h| {
14240            let a = h.prop_attrs(&obj, &key);
14241            let mut m: IndexMap<String, Value> = IndexMap::new();
14242            m.insert("get".into(), get.unwrap_or(Value::Undef));
14243            m.insert("set".into(), set.unwrap_or(Value::Undef));
14244            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14245            m.insert("configurable".into(), Value::Bool(a.configurable));
14246            h.new_object(m)
14247        }));
14248    }
14249    let val = with_host(|h| match h.get(&obj) {
14250        // A Buffer's own properties are exactly its byte indices, read out of the
14251        // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14252        // that V8 keeps on the prototype, so they own no descriptor.
14253        Some(JsObj::Object(p))
14254            if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14255        {
14256            match (
14257                p.get("@@bytes").and_then(|b| h.get(b)),
14258                key.parse::<usize>(),
14259            ) {
14260                (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14261                _ => None,
14262            }
14263        }
14264        Some(JsObj::Object(p)) => p.get(&key).cloned(),
14265        // An array's index keys read the elements; `length` is the exotic own
14266        // property; anything else is an ordinary own key in the side table.
14267        Some(JsObj::Array(items)) => match key.parse::<usize>() {
14268            // An ELIDED index owns no property at all, so it has no descriptor.
14269            Ok(i) if h.is_hole(&obj, i) => None,
14270            Ok(i) => items.get(i).cloned(),
14271            Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14272            Err(_) => h.fn_prop(&obj, &key),
14273        },
14274        // A function/class own prop lives in the fn-prop side table.
14275        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14276        _ => None,
14277    });
14278    match val {
14279        Some(v) => Ok(with_host(|h| {
14280            let a = h.prop_attrs(&obj, &key);
14281            let mut m: IndexMap<String, Value> = IndexMap::new();
14282            m.insert("value".into(), v);
14283            m.insert("writable".into(), Value::Bool(a.writable));
14284            m.insert("enumerable".into(), Value::Bool(a.enumerable));
14285            m.insert("configurable".into(), Value::Bool(a.configurable));
14286            h.new_object(m)
14287        })),
14288        None => Ok(Value::Undef),
14289    }
14290}
14291
14292/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14293/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14294/// the standard "clone with accessors intact" idiom, so this must agree
14295/// key-for-key with `getOwnPropertyNames`.
14296fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14297    let obj = arg0(&args);
14298    let names = object_keys(vec![obj.clone()], 3)?;
14299    let keys: Vec<String> = with_host(|h| match h.get(&names) {
14300        Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14301        _ => Vec::new(),
14302    });
14303    let mut out: IndexMap<String, Value> = IndexMap::new();
14304    for k in keys {
14305        let ks = with_host(|h| h.new_str(k.clone()));
14306        let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14307        if !matches!(d, Value::Undef) {
14308            out.insert(k, d);
14309        }
14310    }
14311    Ok(with_host(|h| h.new_object(out)))
14312}
14313
14314/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14315/// Proxy's `has` trap is user code and may throw.
14316pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14317    if let Some(b) = crate::proxy::has(obj, key)? {
14318        return Ok(b);
14319    }
14320    Ok(has_property_ordinary(obj, key))
14321}
14322
14323/// `[[HasProperty]]` for every non-Proxy receiver.
14324fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14325    // `key in globalThis`: membership matches what the READ answers, which for
14326    // the global object includes every lazily-bound builtin and every global a
14327    // script created. `'Math' in globalThis` and `'x' in globalThis` after
14328    // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14329    // read back fine.
14330    if with_host(|h| h.is_global_object(obj))
14331        && !CJS_WRAPPER_LOCALS.contains(&key)
14332        && global_object_binding(key).is_some()
14333    {
14334        return true;
14335    }
14336    // `key in <builtin namespace/prototype>`: membership matches what a property
14337    // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14338    // prototype methods) resolve as callable thunks via `namespace_property`, so
14339    // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14340    // with the `in` operator before reading the intrinsic).
14341    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14342        return !matches!(namespace_property(&ns, key), Value::Undef);
14343    }
14344    // An integer index of a typed array / Buffer is an own property, and lives
14345    // in the hidden element array rather than the property map — the same
14346    // question `hasOwnProperty` answers, through the same helper. Only a hit
14347    // short-circuits: a non-index key like `'length'` must still fall through
14348    // to the ordinary chain lookup below.
14349    if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14350        return true;
14351    }
14352    if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14353        return true;
14354    }
14355    if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14356        return true;
14357    }
14358    // A member patched onto the receiver's intrinsic prototype. The READ
14359    // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14360    // function while `'at' in []` stayed false.
14361    if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14362        return true;
14363    }
14364    if with_host(|h| match h.get(obj) {
14365        Some(JsObj::Object(p)) => p.contains_key(key),
14366        Some(JsObj::Array(items)) => {
14367            key == "length"
14368                || key
14369                    .parse::<usize>()
14370                    .map(|i| i < items.len() && !h.is_hole(obj, i))
14371                    .unwrap_or(false)
14372                // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14373                // side table, and `in` must see it.
14374                || h.fn_prop(obj, key).is_some()
14375        }
14376        Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14377        // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14378        // the `RegExpObj` struct rather than a property map, so nothing above
14379        // can see it.
14380        Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14381        _ => false,
14382    }) {
14383        return true;
14384    }
14385    // An INHERITED builtin prototype method. These are not objects on the
14386    // prototype chain — they are synthesized by the read path from the
14387    // intrinsic table — so neither `lookup_chain` nor the property map above
14388    // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14389    // Promise.resolve()` all answered false. That last one is the standard
14390    // thenable test, so the `in` operator disagreed with what a read gives for
14391    // every builtin method of every builtin kind.
14392    inherited_builtin_method(obj, key)
14393}
14394
14395/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14396/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14397/// performs no read, so a getter cannot fire.
14398/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14399/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14400/// `None`.
14401///
14402/// The intrinsic prototypes are namespace handles rather than real objects on
14403/// the chain, so an assignment onto one lands in `builtin_statics` and no
14404/// ordinary chain walk can see it. This is the read side: the receiver's own
14405/// constructor's prototype first, then `Object.prototype`, mirroring
14406/// `inherited_method_owner`'s two-step.
14407pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14408    if with_host(|h| h.has_null_proto(obj)) {
14409        return None;
14410    }
14411    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14412        Some(c) => Some(c),
14413        None if is_arguments(obj) => Some("Object"),
14414        None => with_host(|h| default_ctor_name(h, obj)),
14415    };
14416    // Only the side table is consulted, never the real prototype OBJECT's map:
14417    // `String.prototype` and friends are materialized with their intrinsic
14418    // members present, so reading their maps here would re-route every ordinary
14419    // `"a".toString()` through this path — which recursed until the stack blew.
14420    // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14421    // table precisely so the read side can stay this narrow.
14422    let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14423    let found = ctor.and_then(on).or_else(|| on("Object"))?;
14424    // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14425    // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14426    // name back into the table. Dispatching to it would re-enter this lookup
14427    // and recurse until the stack blew, so a thunk that is already this key's
14428    // own intrinsic reports nothing and the ordinary builtin path answers.
14429    let self_thunk = with_host(
14430        |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14431    );
14432    (!self_thunk).then_some(found)
14433}
14434
14435/// Whether `recv` carries `key` as an OWN property — the guard on
14436/// [`inherited_builtin_static`], since an own property shadows anything
14437/// patched onto a prototype.
14438fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14439    with_host(|h| {
14440        if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14441            return true;
14442        }
14443        match h.get(recv) {
14444            Some(JsObj::Object(p)) => p.contains_key(key),
14445            // An ELIDED index owns nothing — the whole point of a hole is that
14446            // the lookup continues up the chain — so it must not count as a
14447            // shadow here or an inherited value at that index stays invisible.
14448            Some(JsObj::Array(items)) => {
14449                key == "length" || {
14450                    key.parse::<usize>()
14451                        .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14452                }
14453            }
14454            _ => false,
14455        }
14456    })
14457}
14458
14459fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14460    if with_host(|h| h.has_null_proto(obj)) {
14461        return false;
14462    }
14463    if let Some(tag) = crate::stdlib::native_tag(obj) {
14464        if crate::stdlib::instance_has_method(&tag, key) {
14465            return true;
14466        }
14467    }
14468    inherited_method_owner(obj, key).is_some()
14469}
14470
14471/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14472/// whether `Array.prototype`'s methods are still reachable from an array.
14473///
14474/// A builtin's methods are synthesized from the receiver's KIND rather than
14475/// found on a chain, so replacing the prototype could not take them away:
14476/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14477/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14478/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14479/// all still answer, as they do in node.
14480///
14481/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14482/// link at all, so this answers true after one map probe and allocates nothing.
14483pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14484    own_intrinsic_reachable(recv)
14485}
14486
14487fn own_intrinsic_reachable(recv: &Value) -> bool {
14488    // A BOXED primitive needs no special case here: its methods resolve through
14489    // `inherited_method_owner`, which applies the wrapper rule itself.
14490    with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14491}
14492
14493/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14494fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14495    let own = Some(ctor);
14496    let mut cur = recv.clone();
14497    for _ in 0..100 {
14498        let explicit = with_host(|h| h.proto_of(&cur));
14499        let Some(p) = explicit else {
14500            // No explicit link: the implicit prototype is this object's own
14501            // kind's, which is what `recv` is asking about only while `cur` is
14502            // still `recv` itself.
14503            if with_host(|h| h.has_null_proto(&cur)) {
14504                return false;
14505            }
14506            let implicit = with_host(|h| default_ctor_name(h, &cur));
14507            // Every implicit prototype chain ends at `Object.prototype`, so a
14508            // question about `Object` is answered yes by any of them.
14509            return implicit == own || ctor == "Object";
14510        };
14511        if with_host(|h| h.is_null(&p)) {
14512            return false;
14513        }
14514        let hit = with_host(|h| {
14515            own.is_some_and(|c| {
14516                matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14517                    || h.intrinsic_proto_ctor(&p) == Some(c)
14518                    || (c == "Object" && h.object_proto() == p)
14519            })
14520        });
14521        if hit {
14522            return true;
14523        }
14524        // A CLASS prototype object is not linked to the builtin its class
14525        // extends — the `extends` relationship is recorded on the class value —
14526        // so the walk has to cross over there or `class D extends Array {}` ends
14527        // it, and every inherited method of every subclass instance vanishes.
14528        if let Some(builtin) = with_host(|h| {
14529            h.class_owning_proto(&p)
14530                .and_then(|c| h.class_builtin_ancestor(&c))
14531                .map(|b| h.callable_name(&b))
14532        }) {
14533            if own == Some(builtin.as_str()) || ctor == "Object" {
14534                return true;
14535            }
14536        }
14537        cur = p;
14538    }
14539    false
14540}
14541
14542/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14543/// — the complement of [`intrinsic_reachable`], which asks about one known
14544/// constructor.
14545///
14546/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14547/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14548/// it: `o.push(1)` works, because those methods are generic over their receiver
14549/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14550/// worked here). Deciding the owner from the receiver's KIND alone made every
14551/// one of them `undefined` — the same "methods come from the kind, not the
14552/// chain" mistake as the detachment case, in the opposite direction.
14553pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14554    chain_intrinsic_ctors(recv)
14555}
14556
14557fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14558    with_host(|h| chain_intrinsic_ctors_h(h, recv))
14559}
14560
14561/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14562/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14563/// back through `with_host` there aborts the process on a double borrow.
14564pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14565    let mut out: Vec<&'static str> = Vec::new();
14566    let mut cur = recv.clone();
14567    for _ in 0..100 {
14568        let Some(p) = h.proto_of(&cur) else {
14569            break;
14570        };
14571        if h.is_null(&p) {
14572            break;
14573        }
14574        let name = match h.get(&p) {
14575            Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14576            _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14577        };
14578        if let Some(n) = name {
14579            if let Some(c) = crate::arity::PROTO_MEMBERS
14580                .iter()
14581                .map(|(k, _)| *k)
14582                .find(|k| *k == n)
14583            {
14584                if !out.contains(&c) {
14585                    out.push(c);
14586                }
14587            }
14588        }
14589        cur = p;
14590    }
14591    out
14592}
14593
14594/// The constructor whose prototype defines `key` for `obj` — its own if that
14595/// prototype has it, otherwise `Object` — or `None` when neither does.
14596///
14597/// Used both by `in` and by the READ, so the two cannot disagree about which
14598/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14599/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14600pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14601    inherited_method_owner(obj, key)
14602}
14603
14604fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14605    if with_host(|h| h.has_null_proto(obj)) {
14606        return None;
14607    }
14608    // The generated prototype-member table, which unlike the arity table knows
14609    // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14610    // on a Symbol are members but not functions — and about `constructor`.
14611    // A BOXED primitive reports its wrapper's constructor, not `Object` —
14612    // `'description' in Object(Symbol())` is true. The box is an ordinary
14613    // object carrying the primitive in a slot, so the ctor comes from what it
14614    // holds rather than from the box itself.
14615    let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14616        Some(c) => Some(c),
14617        // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14618        // spread and `for-of` work, but node's is an exotic that inherits from
14619        // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14620        // its backing kind would hand it the whole `Array.prototype`.
14621        None if is_arguments(obj) => Some("Object"),
14622        None => with_host(|h| default_ctor_name(h, obj)),
14623    };
14624    let on_proto = |c: &str| {
14625        crate::arity::PROTO_MEMBERS
14626            .binary_search_by(|(k, _)| (*k).cmp(c))
14627            .ok()
14628            .is_some_and(|i| {
14629                crate::arity::PROTO_MEMBERS[i]
14630                    .1
14631                    .iter()
14632                    .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14633            })
14634    };
14635    // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14636    // well-known symbol member is absent from it. For an object whose CHAIN
14637    // reaches an intrinsic prototype the intrinsic table has to be consulted as
14638    // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14639    // at all. It is deliberately NOT consulted for the receiver's own kind:
14640    // there a thunk would be minted for every `@@` member the table names,
14641    // including ones whose dispatch has no implementation for that receiver,
14642    // and `[...buffer]` then failed with `@@iterator is not a function`.
14643    //
14644    // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14645    // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14646    // and minting a thunk there produced `@@iterator is not a function` for
14647    // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14648    // fast path, which the table entry would shadow.
14649    let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14650        && crate::stdlib::native_tag(obj).is_none();
14651    let on_proto_or_symbol =
14652        |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14653    // Each candidate is only an answer while ITS prototype is still on the
14654    // receiver's chain. The two are asked separately: replacing an array's
14655    // prototype with a plain object takes `Array.prototype`'s methods away and
14656    // leaves `Object.prototype`'s, since the replacement inherits from it.
14657    if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14658        return Some(c);
14659    }
14660    // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14661    // kind does not account for.
14662    if let Some(c) = chain_intrinsic_ctors(obj)
14663        .into_iter()
14664        .find(|c| on_proto_or_symbol(c))
14665    {
14666        return Some(c);
14667    }
14668    // Everything else inherits `Object.prototype`'s.
14669    if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14670        return Some("Object");
14671    }
14672    None
14673}
14674
14675/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14676/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14677/// that preserves the *reference graph*. Two properties pointing at the same
14678/// object clone to two properties pointing at the same clone, and a cycle clones
14679/// to a cycle instead of recursing forever. `seen` maps each source heap index
14680/// to its clone, which is what buys both.
14681/// The rendering node puts in a `DataCloneError` for a value the structured
14682/// clone algorithm refuses, or `None` when the value IS cloneable.
14683///
14684/// Refusing at all is the point: these used to be copied through by reference,
14685/// so `structuredClone({f: () => 1})` handed back an object sharing the
14686/// original's function and `structuredClone(new WeakMap())` returned the very
14687/// same WeakMap. Node throws on every one of them.
14688fn clone_refusal(v: &Value) -> Option<String> {
14689    let kind = with_host(|h| h.kind_of(v))?;
14690    let render = |ctor: &str| Some(format!("#<{ctor}>"));
14691    match kind {
14692        // A function renders as its SOURCE TEXT here, which each FuncDef
14693        // keeps as a span into its script (`JsHost::func_source`).
14694        ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14695            Some(with_host(|h| h.str_of(v)))
14696        }
14697        ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14698            Some(with_host(|h| h.str_of(v)))
14699        }
14700        ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14701        ObjKind::Promise => render("Promise"),
14702        ObjKind::Generator => Some("[object Generator]".to_string()),
14703        // A proxy is refused by its TARGET's shape: a callable one renders like
14704        // the function it wraps, everything else as a plain object.
14705        ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14706            with_host(|h| h.str_of(v))
14707        } else {
14708            "#<Object>".to_string()
14709        }),
14710        ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14711            render("WeakMap")
14712        }
14713        ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14714            render("WeakSet")
14715        }
14716        _ => match crate::stdlib::native_tag(v).as_deref() {
14717            Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14718            _ => None,
14719        },
14720    }
14721}
14722
14723/// `structuredClone(value[, { transfer }])`.
14724///
14725/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14726/// after the clone — its bytes belong to the copy. The option used to be
14727/// ignored entirely, so the source buffer stayed usable where node leaves it
14728/// with zero length.
14729fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14730    let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14731        Some(opts) => {
14732            let t = get_property(opts, "transfer")?;
14733            if matches!(t, Value::Undef) {
14734                Vec::new()
14735            } else {
14736                host::iter_all(&t)?
14737            }
14738        }
14739        None => Vec::new(),
14740    };
14741    for item in &list {
14742        if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14743            return Err(host::dom_error(
14744                "DataCloneError",
14745                "Found invalid value in transferList.",
14746            ));
14747        }
14748    }
14749    let out = deep_clone(&arg0(&args))?;
14750    for item in &list {
14751        crate::stdlib::typedarray::detach_buffer(item);
14752    }
14753    Ok(out)
14754}
14755
14756pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14757    deep_clone_seen(v, &mut std::collections::HashMap::new())
14758}
14759
14760fn deep_clone_seen(
14761    v: &Value,
14762    seen: &mut std::collections::HashMap<u32, Value>,
14763) -> Result<Value, String> {
14764    let idx = match v {
14765        Value::Obj(i) => *i,
14766        _ => return Ok(v.clone()),
14767    };
14768    if let Some(done) = seen.get(&idx) {
14769        return Ok(done.clone());
14770    }
14771    if crate::stdlib::typedarray::is_detached(v) {
14772        return Err(host::dom_error(
14773            "DataCloneError",
14774            "An ArrayBuffer is detached and could not be cloned.",
14775        ));
14776    }
14777    if let Some(render) = clone_refusal(v) {
14778        return Err(host::dom_error(
14779            "DataCloneError",
14780            &format!("{render} could not be cloned."),
14781        ));
14782    }
14783    // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14784    // handing back the same object let a write through the clone move the
14785    // original's match cursor.
14786    if let Some((src, flags)) = with_host(|h| match h.get(v) {
14787        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14788        _ => None,
14789    }) {
14790        let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14791        let out = regexp_ctor(&args)?;
14792        seen.insert(idx, out.clone());
14793        return Ok(out);
14794    }
14795    Ok(match with_host(|h| h.get(v).cloned()) {
14796        Some(JsObj::Array(items)) => {
14797            // Register the (empty) clone BEFORE recursing so a self-reference
14798            // resolves to it.
14799            let out = with_host(|h| h.new_array(Vec::new()));
14800            seen.insert(idx, out.clone());
14801            let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14802            for x in &items {
14803                cloned.push(deep_clone_seen(x, seen)?);
14804            }
14805            with_host(|h| {
14806                if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14807                    *a = cloned;
14808                }
14809                // A sparse source clones to an equally sparse array: the clone
14810                // walks own properties, so a hole is nothing to copy.
14811                h.copy_holes(v, &out, Some);
14812            });
14813            out
14814        }
14815        Some(JsObj::Object(_)) => {
14816            let out = with_host(|h| h.new_object(IndexMap::new()));
14817            seen.insert(idx, out.clone());
14818            // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14819            // walked the property map, where an accessor stores nothing, so
14820            // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14821            // key and a non-enumerable one are dropped, as node drops them.
14822            let is_error = with_host(|h| h.error_to_string(v)).is_some();
14823            let proto = clone_proto(v);
14824            let keeps_proto = !matches!(proto, CloneProto::Plain);
14825            // An ERROR clones its name, message and stack and NOTHING else —
14826            // node drops any other own property, even an enumerable one.
14827            let keys: Vec<String> = if is_error {
14828                // An ERROR clones its name, message and stack and NOTHING else —
14829                // node drops any other own property, even an enumerable one.
14830                ["name", "message", "stack"]
14831                    .iter()
14832                    .filter(|k| has_property(v, k).unwrap_or(false))
14833                    .map(|k| (*k).to_string())
14834                    .collect()
14835            } else if keeps_proto {
14836                // A preserved exotic keeps EVERY own property, including the
14837                // non-enumerable ones and the internal slots — a Date's time
14838                // value, an ArrayBuffer's `byteLength` and byte store, a typed
14839                // array's view. The enumerable-only walk dropped all of those,
14840                // so a cloned Date read `Invalid Date` and a cloned
14841                // ArrayBuffer had no `byteLength`.
14842                with_host(|h| match h.get(v) {
14843                    Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14844                    _ => Vec::new(),
14845                })
14846            } else {
14847                with_host(|h| h.own_enum_key_names(v))
14848            };
14849            let mut cloned: IndexMap<String, Value> = IndexMap::new();
14850            for k in keys {
14851                // An internal slot is read straight out of the map: it is not a
14852                // property, so a `[[Get]]` would not find it.
14853                let val = if k.starts_with("@@") {
14854                    match with_host(|h| match h.get(v) {
14855                        Some(JsObj::Object(p)) => p.get(&k).cloned(),
14856                        _ => None,
14857                    }) {
14858                        Some(val) => val,
14859                        None => continue,
14860                    }
14861                } else {
14862                    get_property(v, &k)?
14863                };
14864                cloned.insert(k, deep_clone_seen(&val, seen)?);
14865            }
14866            // The prototype survives only for the exotics the algorithm knows —
14867            // a Date, an Error, a typed array, a boxed primitive. A USER class
14868            // instance becomes a plain object, which is what node produces;
14869            // keeping every prototype made `structuredClone(new K())
14870            // instanceof K` true.
14871            with_host(|h| {
14872                if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14873                    *p = cloned;
14874                }
14875                match &proto {
14876                    CloneProto::Same => {
14877                        if let Some(p) = h.proto_of(v) {
14878                            h.set_proto(&out, p);
14879                        }
14880                    }
14881                    CloneProto::Ctor(c) => {
14882                        h.ensure_error_protos();
14883                        let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14884                        if let Some(p) = p {
14885                            h.set_proto(&out, p);
14886                        }
14887                        // A Buffer clones to a plain `Uint8Array`, so the native
14888                        // tag has to change with the prototype — left alone,
14889                        // `Buffer.isBuffer` still answered true for the clone.
14890                        // A Buffer clones to a plain `Uint8Array`, so the native
14891                        // tag has to change with the prototype — left alone,
14892                        // `Buffer.isBuffer` answered true for the clone and the
14893                        // brand stayed `[object Object]`. A typed array is
14894                        // tagged `TypedArray` and names its element type in
14895                        // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14896                        if c == "Uint8Array" {
14897                            let tag = h.new_str("TypedArray");
14898                            let kind = h.new_str("Uint8Array");
14899                            if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14900                                p.insert("@@native".into(), tag);
14901                                p.insert("@@kind".into(), kind);
14902                            }
14903                        }
14904                    }
14905                    CloneProto::Plain => {}
14906                }
14907                h.copy_prop_attrs(v, &out);
14908            });
14909            out
14910        }
14911        // Map/Set are structured types: clone the entries, keep the kind.
14912        Some(JsObj::Map { entries, weak }) => {
14913            let out = with_host(|h| {
14914                h.alloc(JsObj::Map {
14915                    entries: IndexMap::new(),
14916                    weak,
14917                })
14918            });
14919            seen.insert(idx, out.clone());
14920            let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14921            for (k, val) in pairs {
14922                let ck = deep_clone_seen(&k, seen)?;
14923                let cv = deep_clone_seen(&val, seen)?;
14924                let _ = map_method(&out, "set", vec![ck, cv]);
14925            }
14926            out
14927        }
14928        Some(JsObj::Set { entries, weak }) => {
14929            let out = with_host(|h| {
14930                h.alloc(JsObj::Set {
14931                    entries: IndexMap::new(),
14932                    weak,
14933                })
14934            });
14935            seen.insert(idx, out.clone());
14936            let vals: Vec<Value> = entries.values().cloned().collect();
14937            for x in vals {
14938                let cx = deep_clone_seen(&x, seen)?;
14939                let _ = set_method(&out, "add", vec![cx]);
14940            }
14941            out
14942        }
14943        // A string, a BigInt and a boxed primitive are immutable enough to
14944        // share; anything left is a value type.
14945        _ => v.clone(),
14946    })
14947}
14948
14949/// Whether a cloned object keeps the source's prototype.
14950///
14951/// The structured clone algorithm reproduces the exotics it knows and turns
14952/// everything else into a plain object — so a `Date` clones to a `Date` and a
14953/// user class instance clones to an `Object`.
14954fn clone_proto(v: &Value) -> CloneProto {
14955    // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14956    // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14957    // `Error`, not `E`.
14958    if with_host(|h| h.error_to_string(v)).is_some() {
14959        let name = get_property(v, "name")
14960            .map(|n| with_host(|h| h.str_of(&n)))
14961            .unwrap_or_else(|_| "Error".into());
14962        let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14963            name
14964        } else {
14965            "Error".to_string()
14966        };
14967        return CloneProto::Ctor(class);
14968    }
14969    match crate::stdlib::native_tag(v).as_deref() {
14970        // A Buffer is not reproduced as a Buffer: node hands back a plain
14971        // `Uint8Array` over the same bytes.
14972        Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14973        Some(_) => CloneProto::Same,
14974        // A boxed primitive keeps its wrapper; anything else — a user class
14975        // instance included — becomes a plain object.
14976        None if wrapped_primitive(v).is_some() => CloneProto::Same,
14977        None => CloneProto::Plain,
14978    }
14979}
14980
14981/// Which prototype a clone gets: the source's, a named builtin's, or none.
14982enum CloneProto {
14983    Same,
14984    Ctor(String),
14985    Plain,
14986}
14987
14988// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14989
14990/// A short `Name: message` string for an error value (used when an await
14991/// rejection unwinds as a thrown error).
14992pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14993    if let Some(JsObj::Object(props)) = h.get(v) {
14994        let name = props
14995            .get("name")
14996            .map(|x| h.str_of(x))
14997            .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14998            .unwrap_or_else(|| "Error".into());
14999        if let Some(m) = props.get("message") {
15000            return format!("{name}: {}", h.str_of(m));
15001        }
15002        return name;
15003    }
15004    h.str_of(v)
15005}
15006
15007/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
15008/// value)`, or `() => { throw reason }` on the reject path.
15009///
15010/// Returning the carried value directly — what this used to do — skipped both
15011/// halves. A promise returned by the callback was never awaited, so the
15012/// ordinary async-cleanup shape
15013///
15014/// ```text
15015/// work().finally(() => closeConnection()).then(next)
15016/// ```
15017///
15018/// ran `next` before the connection had closed. And the chain settled three
15019/// microtask ticks early, which is observable in ordering against any other
15020/// chain, not just against a timer.
15021///
15022/// A rejection from the callback's own promise wins over the carried value, so
15023/// no reject handler is attached here: it propagates on its own.
15024fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
15025    // PromiseResolve (27.2.4.7) returns an argument that is already a promise
15026    // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
15027    // thenable takes to adopt it, which showed up as a callback returning a
15028    // rejected promise settling one tick late against every other chain.
15029    let p = match with_host(|h| h.promise_id(&result)) {
15030        Some(_) => result,
15031        None => {
15032            let fresh = with_host(|h| h.new_promise());
15033            if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
15034                host::resolve_promise_val(pid, result);
15035            }
15036            fresh
15037        }
15038    };
15039    let cell = with_host(|h| h.new_array(vec![carried]));
15040    let idx = match cell {
15041        Value::Obj(i) => i,
15042        _ => 0,
15043    };
15044    let tag = if rethrow { "finrethrow" } else { "finret" };
15045    let thunk = make_builtin(format!("@@{tag}:{idx}"));
15046    host::promise_then(&p, thunk, Value::Undef)
15047}
15048
15049fn make_builtin(name: String) -> Value {
15050    with_host(|h| h.alloc(JsObj::Builtin(name)))
15051}
15052
15053/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
15054/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
15055///
15056/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
15057/// an EXPLICIT link, so an object on the default prototype reported `null`:
15058/// `({}).__proto__ === Object.prototype` was false while
15059/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
15060/// the three cannot drift apart again.
15061pub fn prototype_of(v: &Value) -> Value {
15062    // Constructor-side inheritance: `Buffer extends Uint8Array`, so
15063    // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
15064    // not `Function.prototype`. This is the class-side half of the subclass
15065    // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
15066    if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
15067        return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
15068    }
15069    // The NativeError constructors inherit from `Error` (20.5.6.2: their
15070    // `[[Prototype]]` is %Error%), so `Object.getPrototypeOf(RangeError) ===
15071    // Error`. They reported `null`.
15072    if matches!(
15073        with_host(|h| h.get(v).cloned()),
15074        Some(JsObj::Builtin(ref n)) if matches!(
15075            n.as_str(),
15076            "EvalError" | "RangeError" | "ReferenceError" | "SyntaxError" | "TypeError"
15077                | "URIError" | "AggregateError"
15078        )
15079    ) {
15080        return with_host(|h| h.alloc(JsObj::Builtin("Error".into())));
15081    }
15082    // Constructor-side inheritance for a `class B extends A` (ClassDefinition
15083    // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
15084    // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
15085    // `ClassVal.parent`, but the link itself was invisible, so
15086    // `Object.getPrototypeOf(B) === A` read false and any library walking the
15087    // constructor chain — rather than calling a static — saw a base class.
15088    // A base class keeps the default answer below (`Function.prototype`).
15089    if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
15090        if let Some(parent) = c.parent {
15091            return parent;
15092        }
15093    }
15094    // `Object.create(null)` and friends really do have a null prototype.
15095    if with_host(|h| h.has_null_proto(v)) {
15096        return with_host(|h| h.null());
15097    }
15098    // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
15099    // reported itself, because the fallback below answers by constructor name
15100    // and a plain object's is `Object` — an infinite chain to anything walking
15101    // it.
15102    if with_host(|h| h.strict_eq(v, &h.object_proto())) {
15103        return with_host(|h| h.null());
15104    }
15105    // Every OTHER builtin prototype namespace (`Array.prototype`,
15106    // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15107    // would send it back to a namespace handle for its own constructor.
15108    if matches!(
15109        with_host(|h| h.get(v).cloned()),
15110        Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15111    ) {
15112        return with_host(|h| h.object_proto());
15113    }
15114    if let Some(p) = with_host(|h| h.proto_of(v)) {
15115        return p;
15116    }
15117    // A builtin exotic with no explicit `[[Prototype]]` link reports its
15118    // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15119    // Array.prototype`), which `strict_eq` compares by name. A plain object
15120    // reports the one real `Object.prototype` object.
15121    with_host(|h| {
15122        h.ensure_native_protos();
15123        match default_ctor_name(h, v) {
15124            Some("Object") => h.object_proto(),
15125            // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15126            // primitive must report that object and not a fresh namespace
15127            // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15128            // compares a thunk against the real object and reads false.
15129            Some(c) => h
15130                .native_proto(c)
15131                .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15132            // A builtin FUNCTION (`Error`, `Math.max`, `parseInt`) is an ordinary
15133            // function object whose `[[Prototype]]` is `Function.prototype`.
15134            None if h.type_of(v) == "function" => h
15135                .native_proto("Function")
15136                .unwrap_or_else(|| h.alloc(JsObj::Builtin("Function.prototype".into()))),
15137            None => h.null(),
15138        }
15139    })
15140}
15141
15142/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15143/// internal resolve/reject functions.
15144/// A fresh promise built through the SPECIES constructor, when a `Promise`
15145/// static was reached through a subclass.
15146///
15147/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15148/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15149/// plain promise, so nothing a subclass produced was an instance of it. The
15150/// executor is a no-op: the result is settled through its promise id, which is
15151/// what the ordinary path does too.
15152fn promise_species_create() -> Result<Option<Value>, String> {
15153    let Some(ctor) = host::current_static_this() else {
15154        return Ok(None);
15155    };
15156    if !matches!(
15157        with_host(|h| h.kind_of(&ctor)),
15158        Some(ObjKind::Class) | Some(ObjKind::Func)
15159    ) {
15160        return Ok(None);
15161    }
15162    let species = match get_property(&ctor, "@@species") {
15163        Ok(Value::Undef) => ctor,
15164        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15165        Ok(s) => s,
15166        Err(_) => ctor,
15167    };
15168    if !matches!(
15169        with_host(|h| h.kind_of(&species)),
15170        Some(ObjKind::Class) | Some(ObjKind::Func)
15171    ) {
15172        return Ok(None);
15173    }
15174    let noop = make_builtin("@@pnoop".to_string());
15175    let p = host::construct(&species, vec![noop])?;
15176    // Only usable if the subclass really produced a promise; a constructor that
15177    // returned something else has no id to settle.
15178    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15179}
15180
15181/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15182/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15183///
15184/// Distinct from `promise_species_create`, which answers for a STATIC reached
15185/// through a subclass. Here the subclass comes from the receiver itself, so
15186/// `P.resolve(1).then(f)` is also a `P`.
15187pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15188    // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15189    // which has no `constructor` entry of its own.
15190    let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15191    if !matches!(
15192        with_host(|h| h.kind_of(&ctor)),
15193        Some(ObjKind::Class) | Some(ObjKind::Func)
15194    ) {
15195        return Ok(None);
15196    }
15197    let species = match get_property(&ctor, "@@species") {
15198        Ok(Value::Undef) => ctor,
15199        Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15200        Ok(s) => s,
15201        Err(_) => ctor,
15202    };
15203    if !matches!(
15204        with_host(|h| h.kind_of(&species)),
15205        Some(ObjKind::Class) | Some(ObjKind::Func)
15206    ) {
15207        return Ok(None);
15208    }
15209    let noop = make_builtin("@@pnoop".to_string());
15210    let p = host::construct(&species, vec![noop])?;
15211    Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15212}
15213
15214fn new_promise(executor: Value) -> Result<Value, String> {
15215    let p = with_host(|h| h.new_promise());
15216    let id = with_host(|h| h.promise_id(&p).unwrap());
15217    let res = make_builtin(format!("@@presolve:{id}"));
15218    let rej = make_builtin(format!("@@preject:{id}"));
15219    if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15220        // A throw in the executor rejects the promise.
15221        let ev = host::take_exc_or_error(&e);
15222        host::reject_promise_val(id, ev);
15223    }
15224    Ok(p)
15225}
15226
15227/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15228/// already-settled promise.
15229pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15230    promise_resolve(v)
15231}
15232
15233fn promise_resolve(v: Value) -> Result<Value, String> {
15234    if let Some(p) = promise_species_create()? {
15235        let id = with_host(|h| h.promise_id(&p).unwrap());
15236        host::resolve_promise_val(id, v);
15237        return Ok(p);
15238    }
15239    Ok(host::promise_of(&v))
15240}
15241fn promise_reject(v: Value) -> Result<Value, String> {
15242    let p = match promise_species_create()? {
15243        Some(p) => p,
15244        None => with_host(|h| h.new_promise()),
15245    };
15246    let id = with_host(|h| h.promise_id(&p).unwrap());
15247    host::reject_promise_val(id, v);
15248    Ok(p)
15249}
15250
15251/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15252/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15253/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15254/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15255/// callers that hand the resolver to an event listener.
15256pub fn pending_promise_with_resolver() -> (Value, Value) {
15257    let p = with_host(|h| h.new_promise());
15258    let id = with_host(|h| h.promise_id(&p).unwrap());
15259    let resolve = make_builtin(format!("@@presolve:{id}"));
15260    (p, resolve)
15261}
15262
15263/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15264///
15265/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15266/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15267/// without the two running together, and it escapes the punctuation that is
15268/// meaningful inside a character class or a group name.
15269fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15270    let v = arg0(&args);
15271    if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15272        return Err(host::type_error("input argument must be a string"));
15273    }
15274    let s = with_host(|h| h.str_of(&v));
15275    // Punctuation that is escaped by CODE POINT rather than with a backslash.
15276    // Measured against node over the whole ASCII range, not taken from a list:
15277    // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15278    const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15279    const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15280    let mut out = String::with_capacity(s.len());
15281    for (i, c) in s.chars().enumerate() {
15282        // A leading ASCII alphanumeric, and only a leading one.
15283        if i == 0 && c.is_ascii_alphanumeric() {
15284            out.push_str(&format!("\\x{:02x}", c as u32));
15285            continue;
15286        }
15287        if SYNTAX.contains(c) {
15288            out.push('\\');
15289            out.push(c);
15290            continue;
15291        }
15292        match c {
15293            '\t' => out.push_str("\\t"),
15294            '\n' => out.push_str("\\n"),
15295            '\u{b}' => out.push_str("\\v"),
15296            '\u{c}' => out.push_str("\\f"),
15297            '\r' => out.push_str("\\r"),
15298            _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15299                let n = c as u32;
15300                if n <= 0xff {
15301                    out.push_str(&format!("\\x{n:02x}"));
15302                } else {
15303                    out.push_str(&format!("\\u{n:04x}"));
15304                }
15305            }
15306            _ => out.push(c),
15307        }
15308    }
15309    Ok(with_host(|h| h.new_str(out)))
15310}
15311
15312/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15313/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15314/// Unicode but not to ECMAScript, and node leaves both alone.
15315fn is_regex_escape_space(c: char) -> bool {
15316    matches!(
15317        c,
15318        '\u{a0}' | '\u{1680}' | '\u{2000}'
15319            ..='\u{200a}'
15320                | '\u{2028}'
15321                | '\u{2029}'
15322                | '\u{202f}'
15323                | '\u{205f}'
15324                | '\u{3000}'
15325                | '\u{feff}'
15326    )
15327}
15328
15329/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15330/// object that merely INHERITS from `Error.prototype` is not one.
15331fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15332    let v = arg0(&args);
15333    Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15334}
15335
15336/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15337/// and `Object.prototype.toString`'s step 9 both test.
15338///
15339/// The brand is the OWN `stack` an error is built with (a `DOMException`
15340/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15341/// neither, which is why inheriting from an error prototype does not make a
15342/// value an error. Shared so the two cannot disagree — branding by a chain
15343/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15344/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15345/// on the same value already said false.
15346pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15347    match h.get(v) {
15348        Some(JsObj::Object(p)) => {
15349            p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15350        }
15351        _ => false,
15352    }
15353}
15354
15355/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15356/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15357/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15358/// costs a tick that this does not.
15359fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15360    let f = arg0(&args);
15361    // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15362    // returns a rejected promise, so the surrounding `try` never sees it.
15363    if !with_host(|h| host::is_callable(h, &f)) {
15364        // Node names the TYPE alongside the value — `number 5 is not a
15365        // function` — which the ordinary call-site message does not. A plain
15366        // object and a symbol name only the type; `null` names both.
15367        let shown = with_host(|h| {
15368            let kind = h.type_of(&f);
15369            match kind {
15370                "undefined" => "undefined".to_string(),
15371                "symbol" | "bigint" => kind.to_string(),
15372                "object" if h.is_null(&f) => "object null".to_string(),
15373                "object" => "object".to_string(),
15374                "string" => format!("string \"{}\"", h.str_of(&f)),
15375                _ => format!("{kind} {}", h.str_of(&f)),
15376            }
15377        });
15378        let p = with_host(|h| h.new_promise());
15379        let id = with_host(|h| h.promise_id(&p).unwrap());
15380        let reject = make_builtin(format!("@@preject:{id}"));
15381        let err =
15382            with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15383        host::invoke(&reject, vec![err], None)?;
15384        return Ok(p);
15385    }
15386    let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15387    let p = with_host(|h| h.new_promise());
15388    let id = with_host(|h| h.promise_id(&p).unwrap());
15389    let resolve = make_builtin(format!("@@presolve:{id}"));
15390    let reject = make_builtin(format!("@@preject:{id}"));
15391    let promise = p;
15392    match host::invoke(&f, rest, None) {
15393        Ok(v) => {
15394            host::invoke(&resolve, vec![v], None)?;
15395        }
15396        Err(e) => {
15397            // The thrown VALUE, not a re-synthesis of its rendering: a callback
15398            // that throws a `TypeError` must reject with that object, and
15399            // rebuilding it from the message string flattened it to a plain
15400            // `Error` whose message was the rendered `Uncaught TypeError: t`.
15401            let err =
15402                with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15403            with_host(|h| {
15404                h.error = None;
15405                h.exc = None;
15406            });
15407            host::invoke(&reject, vec![err], None)?;
15408        }
15409    }
15410    Ok(promise)
15411}
15412
15413fn promise_with_resolvers() -> Result<Value, String> {
15414    let p = with_host(|h| h.new_promise());
15415    let id = with_host(|h| h.promise_id(&p).unwrap());
15416    let resolve = make_builtin(format!("@@presolve:{id}"));
15417    let reject = make_builtin(format!("@@preject:{id}"));
15418    let mut props: IndexMap<String, Value> = IndexMap::new();
15419    props.insert("promise".into(), p);
15420    props.insert("resolve".into(), resolve);
15421    props.insert("reject".into(), reject);
15422    Ok(with_host(|h| h.new_object(props)))
15423}
15424
15425/// A promise already rejected with `e` — what every combinator hands back when
15426/// the ITERABLE misbehaves.
15427///
15428/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15429/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15430/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15431/// and the caller saw the error at the call site instead.
15432fn rejected_promise(e: String) -> Value {
15433    let p = with_host(|h| h.new_promise());
15434    let id = with_host(|h| h.promise_id(&p).unwrap());
15435    let reject = make_builtin(format!("@@preject:{id}"));
15436    let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15437    with_host(|h| {
15438        h.error = None;
15439        h.exc = None;
15440    });
15441    let _ = host::invoke(&reject, vec![err], None);
15442    p
15443}
15444
15445#[derive(Clone, Copy)]
15446enum AllMode {
15447    All,
15448    AllSettled,
15449}
15450
15451/// `Promise.all` / `Promise.allSettled`.
15452fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15453    let items = match host::iter_all(&arg0(&args)) {
15454        Ok(v) => v,
15455        Err(e) => return Ok(rejected_promise(e)),
15456    };
15457    // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15458    // subclass the promise it hands back is an instance of that subclass.
15459    let result = match promise_species_create()? {
15460        Some(p) => p,
15461        None => with_host(|h| h.new_promise()),
15462    };
15463    let rid = with_host(|h| h.promise_id(&result).unwrap());
15464    let n = items.len();
15465    if n == 0 {
15466        let empty = with_host(|h| h.new_array(Vec::new()));
15467        host::resolve_promise_val(rid, empty);
15468        return Ok(result);
15469    }
15470    // Shared mutable accumulator via Rc<RefCell<…>>.
15471    let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15472    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15473    for (i, it) in items.into_iter().enumerate() {
15474        let ap = host::promise_of(&it);
15475        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15476        let slots = slots.clone();
15477        let remaining = remaining.clone();
15478        host::subscribe_native(
15479            aid,
15480            Box::new(move |state, val| {
15481                let settled = match mode {
15482                    AllMode::All => {
15483                        if state == host::PromiseState::Rejected {
15484                            host::reject_promise_val(rid, val);
15485                            return Ok(());
15486                        }
15487                        val
15488                    }
15489                    AllMode::AllSettled => with_host(|h| {
15490                        let mut m: IndexMap<String, Value> = IndexMap::new();
15491                        if state == host::PromiseState::Rejected {
15492                            m.insert("status".into(), h.new_str("rejected"));
15493                            m.insert("reason".into(), val);
15494                        } else {
15495                            m.insert("status".into(), h.new_str("fulfilled"));
15496                            m.insert("value".into(), val);
15497                        }
15498                        h.new_object(m)
15499                    }),
15500                };
15501                slots.borrow_mut()[i] = settled;
15502                let mut r = remaining.borrow_mut();
15503                *r -= 1;
15504                if *r == 0 {
15505                    let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15506                    host::resolve_promise_val(rid, arr);
15507                }
15508                Ok(())
15509            }),
15510        );
15511    }
15512    Ok(result)
15513}
15514
15515/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15516fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15517    let items = match host::iter_all(&arg0(&args)) {
15518        Ok(v) => v,
15519        Err(e) => return Ok(rejected_promise(e)),
15520    };
15521    // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15522    let result = match promise_species_create()? {
15523        Some(p) => p,
15524        None => with_host(|h| h.new_promise()),
15525    };
15526    let rid = with_host(|h| h.promise_id(&result).unwrap());
15527    let n = items.len();
15528    let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15529    let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15530    for (i, it) in items.into_iter().enumerate() {
15531        let ap = host::promise_of(&it);
15532        let aid = with_host(|h| h.promise_id(&ap).unwrap());
15533        let errors = errors.clone();
15534        let remaining = remaining.clone();
15535        host::subscribe_native(
15536            aid,
15537            Box::new(move |state, val| {
15538                if any {
15539                    if state == host::PromiseState::Fulfilled {
15540                        host::resolve_promise_val(rid, val);
15541                    } else {
15542                        errors.borrow_mut()[i] = val;
15543                        let mut r = remaining.borrow_mut();
15544                        *r -= 1;
15545                        if *r == 0 {
15546                            // All rejected → AggregateError carrying every reason.
15547                            let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15548                            let msg = with_host(|h| h.new_str("All promises were rejected"));
15549                            let agg = make_error_inner("AggregateError", &[reasons, msg]);
15550                            host::reject_promise_val(rid, agg);
15551                        }
15552                    }
15553                } else if state == host::PromiseState::Rejected {
15554                    host::reject_promise_val(rid, val);
15555                } else {
15556                    host::resolve_promise_val(rid, val);
15557                }
15558                Ok(())
15559            }),
15560        );
15561    }
15562    Ok(result)
15563}
15564
15565/// `.then` / `.catch` / `.finally` on a promise.
15566fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15567    match name {
15568        "then" => Ok(host::promise_then(
15569            recv,
15570            args.first().cloned().unwrap_or(Value::Undef),
15571            args.get(1).cloned().unwrap_or(Value::Undef),
15572        )),
15573        "catch" => Ok(host::promise_then(
15574            recv,
15575            Value::Undef,
15576            args.first().cloned().unwrap_or(Value::Undef),
15577        )),
15578        "finally" => {
15579            let cb = arg0(&args);
15580            // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15581            // as BOTH handlers, and `then` ignores a non-callable one — so the
15582            // value or reason simply passes through. Building the thunks
15583            // regardless meant `p.finally(null)` tried to call `null`.
15584            if !with_host(|h| host::is_callable(h, &cb)) {
15585                return Ok(host::promise_then(recv, cb.clone(), cb));
15586            }
15587            let i = match cb {
15588                Value::Obj(i) => i,
15589                _ => 0,
15590            };
15591            let pass = make_builtin(format!("@@finpass:{i}"));
15592            let throw = make_builtin(format!("@@finthrow:{i}"));
15593            Ok(host::promise_then(recv, pass, throw))
15594        }
15595        _ => Err(host::type_error(&format!(
15596            "promise.{name} is not a function"
15597        ))),
15598    }
15599}
15600
15601fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15602    with_host(|h| {
15603        if next_tick {
15604            h.queue_nexttick(cb, args);
15605        } else {
15606            h.queue_micro(cb, args);
15607        }
15608    });
15609}
15610
15611/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15612/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15613/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15614///
15615/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15616/// fires, so it runs until cleared and — being referenced — holds the process
15617/// open exactly as in Node.
15618fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15619    let cb = arg0(&args);
15620    let delay = if name == "setImmediate" {
15621        -1.0 // before any 0ms timeout
15622    } else {
15623        args.get(1)
15624            .map(|d| with_host(|h| h.to_number(d)))
15625            .unwrap_or(0.0)
15626            .max(0.0)
15627    };
15628    let extra = if name == "setImmediate" {
15629        args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15630    } else {
15631        args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15632    };
15633    // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15634    // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15635    let interval = (name == "setInterval").then(|| delay.max(1.0));
15636    let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15637    let tag = if name == "setImmediate" {
15638        "Immediate"
15639    } else {
15640        "Timeout"
15641    };
15642    crate::stdlib::timers::new_handle(id, tag)
15643}
15644
15645/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15646/// by the bare id it coerces to (code that stored `+timer` still works).
15647fn clear_timer(v: &Value) {
15648    let id =
15649        crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15650    with_host(|h| h.cancel_timer(id));
15651}