Skip to main content

nodejs/
host.rs

1//! The JavaScript object heap and runtime, reached from fusevm through
2//! registered builtins (`register_builtin`) and the strict numeric hook.
3//!
4//! node-js owns no VM and no JIT: the compiler lowers JS to `fusevm::Chunk`, and
5//! every JS-specific operation the VM can't do natively is a builtin call that
6//! lands here. Local variables live in `Rc<RefCell>` environments chained
7//! parent-to-child, so a nested function/closure captures its enclosing scope by
8//! reference.
9//!
10//! Value representation:
11//!   - immediate: `Value::Float` (every JS number — one IEEE-754 f64 type),
12//!     `Value::Bool` (true/false), `Value::Undef` (undefined);
13//!   - heap `Value::Obj(u32)` handles: string, array, object, function,
14//!     builtin-namespace, and the canonical `null` — the reference types.
15
16use fusevm::{Chunk, NumOp, VMResult, Value, VM};
17use indexmap::IndexMap;
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::rc::Rc;
22use std::sync::mpsc::{Receiver, Sender};
23use std::time::{Duration, Instant};
24
25/// A unit of I/O work handed from a background I/O thread to the main-thread
26/// event loop. It is a boxed closure so `host.rs` stays agnostic of `net`/`http`:
27/// the I/O thread captures only plain `Send` data (bytes, ids, `TcpStream`s) and
28/// the closure runs the JS-touching dispatch on the main thread (where the
29/// thread-local host lives). I/O threads NEVER touch the host directly.
30pub type IoTask = Box<dyn FnOnce() -> Result<(), String> + Send>;
31
32/// Builtin ids emitted by the compiler and registered on every VM. The compiler
33/// (`compiler.rs`) and the handler table (`builtins.rs::install`) must agree on
34/// these exactly.
35pub mod ops {
36    pub const GETLOCAL: u16 = 1; // [name] -> value (scope-chain read)
37    pub const SETLOCAL: u16 = 2; // [name, value] -> value (assignment)
38    pub const DECLARE: u16 = 3; // [name, value] -> value (let/const/var into current scope)
39    pub const DELNAME: u16 = 4; // [name]
40    pub const GETATTR: u16 = 5; // [recv, name] -> value (member .x)
41    pub const SETATTR: u16 = 6; // [recv, name, value]
42    pub const GETITEM: u16 = 7; // [recv, idx] -> value (computed [k])
43    pub const SETITEM: u16 = 8; // [recv, idx, value]
44    pub const DELITEM: u16 = 9; // [recv, idx] -> Bool (delete obj[k])
45    pub const MKSTR: u16 = 10; // [parts...] -> str (concat)
46    pub const MKARR: u16 = 11; // [items...] -> array
47    pub const MKOBJ: u16 = 12; // [tag,k,v,...] -> object (tag 1 = ...spread of k)
48    pub const CALL: u16 = 13; // [name, args...] -> resolve name & call
49    pub const CALL_METHOD: u16 = 14; // [recv, name, args...]
50    pub const CALL_VALUE: u16 = 15; // [callable, args...]
51    pub const NEW: u16 = 16; // [ctor, args...] -> instance
52    pub const TRUTHY: u16 = 17; // [v] -> Bool (JS truthiness)
53    pub const TOSTR: u16 = 18; // [v] -> str via String(v)
54    pub const MKFUNC: u16 = 19; // [func_id, defaults...] -> closure
55    pub const GETITER: u16 = 20; // [iterable] -> iterator (left on stack)
56    pub const FORITER: u16 = 21; // peek iterator -> pushes value + Bool(has_next)
57    pub const FORIN_KEYS: u16 = 22; // [obj] -> array of enumerable keys
58    pub const CONTAINS: u16 = 23; // [key, obj] -> Bool (`in`)
59    pub const SIG_RETURN: u16 = 24; // [v] -> return v from the function
60    pub const BINOP: u16 = 25; // [tag, a, b] -> bitwise/shift result (JS int32 semantics)
61    pub const UNARY: u16 = 26; // [tag, v] -> unary +/~ result
62    pub const STRICT_EQ: u16 = 27; // [a, b] -> Bool (===)
63    pub const LOOSE_EQ: u16 = 28; // [a, b] -> Bool (==)
64    pub const TYPEOF: u16 = 29; // [v] -> str
65    pub const LOAD_NULL: u16 = 30; // [] -> the canonical null
66    pub const THROW: u16 = 31; // [v] -> throw
67    pub const TRY: u16 = 32; // [try_id] -> run a try/catch/finally block
68    pub const NULLISH: u16 = 33; // [v] -> Bool (v is null or undefined)
69    pub const UNPACK: u16 = 34; // [iterable, count, star] -> pushes count values
70    pub const BUILD_ARGS: u16 = 35; // [tag,val,...] -> flat array (tag 1 = ...spread)
71    pub const THIS: u16 = 36; // [] -> current `this`
72    pub const INSTANCEOF: u16 = 37; // [a, b] -> Bool
73    pub const DELPROP_NAME: u16 = 38; // [recv, name] -> Bool (delete obj.name)
74    pub const APPLY: u16 = 39; // [callable, argsArray] -> call with spread args
75    pub const APPLY_METHOD: u16 = 40; // [recv, name, argsArray] -> method call with spread
76    pub const OBJ_REST: u16 = 41; // [obj, excludedKeys] -> object of remaining keys
77    pub const DIV: u16 = 42; // [a, b] -> IEEE `a / b` (JS: x/0 = ±Infinity, 0/0 = NaN)
78    pub const MKCLASS: u16 = 43; // [parent_or_undef, ctor_fn] -> class constructor value
79    pub const DEF_MEMBER: u16 = 44; // [class, name, kind, is_static, fn] -> define method/get/set
80    pub const SUPER_CALL: u16 = 45; // [args...] -> invoke parent ctor on `this`, then init fields
81    pub const SUPER_GET: u16 = 46; // [name] -> resolve `super.name` (method up the parent chain)
82    pub const YIELD: u16 = 47; // [v] -> suspend the running generator, yield v
83    pub const PROPKEY: u16 = 48; // [v] -> property-key string (Symbol -> internal key, else String())
84    pub const NEW_TARGET: u16 = 49; // [] -> the current frame's new.target (undefined if not `new`)
85    pub const DEF_FIELD: u16 = 50; // [class, name, thunk] -> register an instance field initializer
86    pub const AWAIT: u16 = 51; // [v] -> await v (suspend the async coroutine until v settles)
87    pub const DEF_ACCESSOR: u16 = 52; // [obj, name, kind, fn] -> install a getter/setter on obj
88    pub const DBG_LINE: u16 = 53; // [line] -> DAP statement marker (debug only)
89    pub const MKBIGINT: u16 = 54; // [decimal_str] -> heap BigInt value
90    pub const MKREGEX: u16 = 55; // [pattern, flags] -> heap RegExp value
91    pub const TAG_TMPL: u16 = 56; // [tag, cooked..., raw..., n, values...] -> tagged-template call
92    pub const GET_ASYNC_ITER: u16 = 57; // [iterable] -> async iterator (for-await-of)
93    pub const ASYNC_STEP: u16 = 58; // [asyncIterator] -> Promise of {value, done}
94    pub const NUM_STEP: u16 = 59; // [tag(±1), old] -> pushes ToNumeric(old), returns old±1 (type-preserving; BigInt-aware ++/--)
95    pub const ITER_CLOSE: u16 = 60; // [iterator] -> close it (for-of break: run a generator's finally / call .return())
96    pub const TYPEOF_NAME: u16 = 61; // [name] -> str; `typeof <ident>` reads the name WITHOUT throwing (unbound -> "undefined")
97    pub const SIG_BREAK: u16 = 62; // [label|""] -> raise a Break signal and halt the chunk (break out of a `try`)
98    pub const SIG_CONTINUE: u16 = 63; // [label|""] -> raise a Continue signal and halt the chunk
99    pub const SIG_UNWIND: u16 = 64; // [tag] -> 0 none / 1 break here / 2 continue here; halts the chunk to propagate
100    pub const PUSH_SCOPE: u16 = 65; // [] -> enter a fresh block scope (`let`/`const` live here)
101    pub const POP_SCOPE: u16 = 66; // [] -> leave the innermost block scope
102    pub const COPY_SCOPE: u16 = 67; // [] -> replace the innermost block scope with a COPY (per-iteration `let`)
103    pub const DECLARE_VAR: u16 = 68; // [name, value] -> declare at FUNCTION scope, ignoring block scopes (`var`)
104    pub const NAMED_EVAL: u16 = 69; // [key, kind, fn] -> fn; SetFunctionName for a COMPUTED key (kind picks the `get `/`set ` prefix)
105    pub const POW: u16 = 70; // [a, b] -> JS `a ** b` (NOT native `Op::Pow`: IEEE pow answers 1 for `(-1) ** Infinity` and `1 ** NaN`)
106    pub const DECLARE_CONST: u16 = 71; // [name, value] -> value; like DECLARE but the binding is IMMUTABLE (`const`)
107    pub const MARK_HOLE: u16 = 72; // [arr, index] -> arr; record an ELIDED array-literal element
108    pub const SETLOCAL_STRICT: u16 = 73; // [name, value] -> value; like SETLOCAL but an UNRESOLVABLE name throws ReferenceError instead of creating a global (strict-mode PutValue)
109    pub const HOIST_VAR: u16 = 74; // [name] -> create the `var` binding as undefined IF ABSENT (hoisting)
110    pub const FORIN_ALIVE: u16 = 75; // [obj, key] -> Bool; is `key` STILL an enumerable property of `obj`? (a `for-in` body may have deleted it)
111    pub const HOIST_TDZ: u16 = 76; // [name] -> declare `name` in the CURRENT scope as UNINITIALIZED (the `let`/`const`/`class` temporal dead zone)
112    pub const NEW_SPREAD: u16 = 77; // [ctor, argsArray] -> instance; `new C(...xs)`, where the argument list is built at run time
113    pub const SUPER_CALL_SPREAD: u16 = 78; // [argsArray] -> invoke the parent ctor with a run-time argument list (`super(...xs)`)
114}
115
116/// Per-call-site callee SOURCE TEXT, for the `TypeError` a failed call raises.
117///
118/// V8 reports the callee the way the source wrote it — `z.f is not a function`,
119/// not `f is not a function` — by re-printing the AST of the call it was
120/// evaluating. The text is therefore a static property of the SITE, so the
121/// compiler records it once per call op and nothing is carried at run time: the
122/// table is consulted only on the error path.
123///
124/// Keyed by the chunk's `op_hash` (which `ChunkBuilder::build` computes anyway)
125/// paired with the op index. `op_hash` covers the op vector but not the name
126/// pool, so two chunks that compile to the same ops with different names share a
127/// key; the consequence is confined to which receiver text an error message
128/// names, never to what a program does.
129mod call_sites {
130    use std::cell::RefCell;
131
132    thread_local! {
133        pub(super) static SITES: RefCell<rustc_hash::FxHashMap<(u64, usize), String>> =
134            RefCell::new(rustc_hash::FxHashMap::default());
135    }
136
137    /// Record every call site of a freshly built chunk.
138    pub fn register(op_hash: u64, sites: Vec<(usize, String)>) {
139        if sites.is_empty() {
140            return;
141        }
142        SITES.with(|m| {
143            let mut m = m.borrow_mut();
144            for (ip, text) in sites {
145                m.insert((op_hash, ip), text);
146            }
147        });
148    }
149
150    /// The callee text recorded for the op at `ip` of the chunk `op_hash`.
151    pub fn text(op_hash: u64, ip: usize) -> Option<String> {
152        SITES.with(|m| m.borrow().get(&(op_hash, ip)).cloned())
153    }
154
155    pub fn clear() {
156        SITES.with(|m| m.borrow_mut().clear());
157    }
158}
159
160pub use call_sites::{clear as clear_call_sites, register as register_call_sites};
161
162/// How many `for…of` / `yield*` iterators are parked on the VM stack at each
163/// `yield` op, recorded by the compiler the same way callee text is.
164///
165/// A `.return()`/`.throw()` injected at a suspension point halts the generator's
166/// chunk outright, which jumps past the loop exits that would have closed those
167/// iterators — so the halt path has to close them itself, and this is how it
168/// knows how many are there and that they are the top of the stack.
169mod yield_sites {
170    use std::cell::RefCell;
171
172    thread_local! {
173        pub(super) static DEPTHS: RefCell<rustc_hash::FxHashMap<(u64, usize), usize>> =
174            RefCell::new(rustc_hash::FxHashMap::default());
175    }
176
177    pub fn register(op_hash: u64, sites: Vec<(usize, usize)>) {
178        if sites.is_empty() {
179            return;
180        }
181        DEPTHS.with(|m| {
182            let mut m = m.borrow_mut();
183            for (ip, depth) in sites {
184                m.insert((op_hash, ip), depth);
185            }
186        });
187    }
188
189    pub fn depth(op_hash: u64, ip: usize) -> usize {
190        DEPTHS.with(|m| m.borrow().get(&(op_hash, ip)).copied().unwrap_or(0))
191    }
192
193    pub fn clear() {
194        DEPTHS.with(|m| m.borrow_mut().clear());
195    }
196}
197
198pub use yield_sites::{clear as clear_yield_sites, register as register_yield_sites};
199
200/// Every call site and yield site registered so far, as the cache stores them:
201/// `(op_hash, ip)` keys with their recorded value.
202///
203/// The tables are built by the COMPILER (`finish_chunk`), so a run that loads a
204/// program from the bytecode cache never fills them — and everything that reads
205/// them silently degrades: a generator's parked `for…of`/`yield*` iterators are
206/// not closed on an injected `.return()`, so their `finally` never runs, and a
207/// `TypeError` loses the callee's source text. Storing them alongside the
208/// program is what makes a cache hit behave like a compile.
209pub type SiteTables = (Vec<((u64, usize), String)>, Vec<((u64, usize), usize)>);
210
211/// Snapshot both registries.
212pub fn site_tables() -> SiteTables {
213    let calls = call_sites::SITES.with(|m| {
214        m.borrow()
215            .iter()
216            .map(|(k, v)| (*k, v.clone()))
217            .collect::<Vec<_>>()
218    });
219    let yields =
220        yield_sites::DEPTHS.with(|m| m.borrow().iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>());
221    (calls, yields)
222}
223
224/// Put a snapshot back — what a cache hit does in place of compiling.
225pub fn restore_site_tables(t: &SiteTables) {
226    call_sites::SITES.with(|m| {
227        let mut m = m.borrow_mut();
228        for (k, v) in &t.0 {
229            m.insert(*k, v.clone());
230        }
231    });
232    yield_sites::DEPTHS.with(|m| {
233        let mut m = m.borrow_mut();
234        for (k, v) in &t.1 {
235            m.insert(*k, *v);
236        }
237    });
238}
239
240/// The number of loop iterators parked on the stack at the op currently
241/// executing, for the abrupt-completion close in `b_yield`.
242pub fn parked_iters(vm: &fusevm::VM) -> usize {
243    yield_sites::depth(vm.chunk.op_hash, vm.ip.saturating_sub(1))
244}
245
246/// Rewrite a `<subject> is not a function` / `is not a constructor` message with
247/// the SOURCE TEXT of the callee at the currently executing op, as V8 does.
248///
249/// `subject` is what the raising code named — the method name, or the callee's
250/// rendered value. The message's own subject must END WITH it, which is the
251/// guard that keeps an unrelated error raised deeper inside a native method from
252/// being relabelled with this call's text. (A native dispatcher may prefix its
253/// own receiver word, e.g. `map.get is not a function`, so the whole subject is
254/// replaced rather than trimmed by length.)
255///
256/// Returns the message unchanged when no site was recorded, so a shape the
257/// printer declines to print keeps the old wording rather than an invented one.
258/// The source text recorded for the op currently executing, if any. `vm.ip` has
259/// already advanced past it.
260pub fn call_site_text(vm: &fusevm::VM) -> Option<String> {
261    call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1))
262}
263
264pub fn name_call_site(vm: &fusevm::VM, subject: &str, msg: String) -> String {
265    for tail in [
266        " is not a function",
267        " is not a constructor",
268        " is not iterable",
269    ] {
270        let Some(head) = msg.strip_suffix(tail) else {
271            continue;
272        };
273        // The prefix is the error class (`TypeError: `); the rest is the subject.
274        // The FIRST separator, not the last: a rendered VALUE can contain one —
275        // `{ a: 1 } is not iterable` split at the last `": "` left the subject
276        // as `1 }`, which matched nothing and silently skipped the rename.
277        let (prefix, found) = match head.find(": ") {
278            Some(i) => (&head[..i + 2], &head[i + 2..]),
279            None => ("", head),
280        };
281        if !found.ends_with(subject) {
282            return msg;
283        }
284        // `vm.ip` has already advanced past the op being executed.
285        let Some(text) = call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1)) else {
286            return msg;
287        };
288        return format!("{prefix}{text}{tail}");
289    }
290    msg
291}
292
293/// `SIG_UNWIND` scope tags: what the emitting site is nested in.
294pub mod unwind {
295    /// No enclosing loop in this chunk — any pending signal propagates outward.
296    pub const NO_LOOP: &str = "";
297    /// An enclosing UNLABELED loop in this chunk.
298    pub const PLAIN_LOOP: &str = "\u{0}";
299    /// `SIG_UNWIND` result codes.
300    pub const NONE: i64 = 0;
301    pub const BREAK: i64 = 1;
302    pub const CONTINUE: i64 = 2;
303}
304
305/// `DEF_MEMBER` member-kind tags.
306pub mod member {
307    pub const METHOD: i64 = 0;
308    pub const GET: i64 = 1;
309    pub const SET: i64 = 2;
310    /// A static FIELD (`static x = 1`), which is a data property of the
311    /// constructor rather than a method. Only distinguished from `METHOD` for a
312    /// PRIVATE name, where the declaration must install the private element
313    /// without tripping the brand check an ordinary write to `#x` gets — and
314    /// where node's brand-check message words a field differently from a method.
315    pub const STATIC_FIELD: i64 = 3;
316}
317
318/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
319pub mod binop {
320    pub const BITAND: i64 = 0;
321    pub const BITOR: i64 = 1;
322    pub const BITXOR: i64 = 2;
323    pub const SHL: i64 = 3;
324    pub const SHR: i64 = 4;
325    pub const USHR: i64 = 5;
326}
327
328/// Unary op tags carried by `ops::UNARY`.
329pub mod unop {
330    pub const POS: i64 = 0; // unary +
331    pub const BITNOT: i64 = 1; // ~
332}
333
334// ── heap objects ───────────────────────────────────────────────────────────
335
336/// A compiled function template: parameter shape + body chunk. Shared by every
337/// closure created from the same function/arrow.
338#[derive(Clone, serde::Serialize, serde::Deserialize)]
339pub struct FuncDef {
340    pub name: String,
341    /// Parameter binding templates (destructuring lowered by the compiler into
342    /// the body prologue; here we only track the simple arg slots).
343    pub params: Vec<ParamSlot>,
344    pub chunk: Chunk,
345    pub is_arrow: bool,
346    /// True for a `function*`/`*method`/generator arrow: calling it builds a
347    /// suspended generator instead of running the body.
348    pub is_generator: bool,
349    /// True for an `async` function/method/arrow: calling it drives a coroutine
350    /// and returns a Promise; `await` inside suspends via the same yielder.
351    pub is_async: bool,
352    /// True when the function body (or the enclosing program) is strict. A
353    /// SLOPPY function called with no receiver gets the GLOBAL object as
354    /// `this`; a strict one keeps `undefined` (10.2.1.2 OrdinaryCallBindThis).
355    #[serde(default)]
356    pub strict: bool,
357    /// True for a MethodDefinition (`{ m(){} }`, a class method/accessor). A
358    /// non-generator method is not a constructor, so it owns no `prototype`.
359    #[serde(default)]
360    pub is_method: bool,
361    /// True for a NAMED function *expression* (`const f = function fact(n) {…}`):
362    /// the closure gets an extra environment binding its own name to itself, so
363    /// the body can recurse through that name even when the outer binding differs.
364    #[serde(default)]
365    pub self_name: bool,
366    /// The definition's byte range in its script (`(0, 0)`: none), for
367    /// `Function.prototype.toString` (20.2.3.5).
368    #[serde(default)]
369    pub span: (u32, u32),
370    /// The host `scripts` entry `span` indexes, set when the program loads.
371    #[serde(default)]
372    pub script: Option<u32>,
373}
374
375/// One parameter slot. `name` is the simple bound name; a destructuring pattern
376/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
377#[derive(Clone, serde::Serialize, serde::Deserialize)]
378pub struct ParamSlot {
379    pub name: String,
380    /// True for the `...rest` collector.
381    pub rest: bool,
382    /// True if this slot has a default expression (applied in the body prologue).
383    pub has_default: bool,
384}
385
386/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
387/// current scope.
388#[derive(Clone, serde::Serialize, serde::Deserialize)]
389pub struct TryDef {
390    pub block: Chunk,
391    /// `(catch_param_name, catch_body)`.
392    pub handler: Option<(Option<String>, Chunk)>,
393    pub finalizer: Option<Chunk>,
394}
395
396/// A live closure value.
397#[derive(Clone)]
398pub struct FuncVal {
399    pub def_id: usize,
400    /// Captured lexical environment (enclosing scope chain), for free vars.
401    pub env: Option<Env>,
402    /// `this` captured at definition time (arrow functions).
403    pub this: Option<Value>,
404    pub is_arrow: bool,
405    /// The owning class name for a method (drives `super` resolution). `None` for
406    /// plain functions/arrows.
407    pub home_class: Option<String>,
408    /// Whether that method is a STATIC one. `super.x` resolves against a
409    /// different object in each case — the parent constructor for a static
410    /// method, the parent's prototype for an instance method — and the class
411    /// name alone cannot tell them apart, since both carry the same one.
412    pub home_static: bool,
413    /// The object literal a shorthand method was defined in, for `super` inside
414    /// it. A class method resolves `super` through `home_class` instead; this
415    /// is the `[[HomeObject]]` an ordinary `{ m() { super.x } }` needs, and
416    /// without it there was nothing to resolve against.
417    pub home_object: Option<Value>,
418}
419
420/// A heap object.
421#[derive(Clone)]
422pub enum JsObj {
423    Str(String),
424    Array(Vec<Value>),
425    Object(IndexMap<String, Value>),
426    Func(FuncVal),
427    /// A first-class reference to a builtin function or namespace
428    /// (`console.log`, `Math`, `parseInt`).
429    Builtin(String),
430    /// A bound method value (`obj.method` captured then called): dispatches
431    /// through `call_method(recv, name, args)` when invoked.
432    BoundMethod {
433        recv: Value,
434        name: String,
435    },
436    /// The single canonical `null`.
437    Null,
438    /// A live iterator over a sequence, with a cursor.
439    Iter {
440        items: Vec<Value>,
441        idx: usize,
442    },
443    /// A bound function (`fn.bind(thisArg, ...preargs)`).
444    BoundFunc {
445        target: Value,
446        this: Value,
447        args: Vec<Value>,
448    },
449    /// A class constructor value: the runtime object produced by a `class`.
450    Class(ClassVal),
451    /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
452    /// key (shared) vs a fresh `Symbol()`.
453    Symbol {
454        desc: Option<String>,
455        id: u64,
456    },
457    /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
458    Map {
459        entries: IndexMap<MapKey, (Value, Value)>,
460        weak: bool,
461    },
462    /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
463    Set {
464        entries: IndexMap<MapKey, Value>,
465        weak: bool,
466    },
467    /// A live generator, backed by a stackful `corosensei` coroutine in
468    /// `JsHost.generators`.
469    Generator {
470        id: u32,
471    },
472    /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
473    Promise {
474        id: u32,
475    },
476    /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
477    BigInt(num_bigint::BigInt),
478    /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
479    RegExp(Box<RegExpObj>),
480    /// A `Proxy`: every essential internal method is diverted to `handler`'s
481    /// traps (see `crate::proxy`). `revoked` is set by the thunk
482    /// `Proxy.revocable` hands back, after which every operation throws.
483    Proxy {
484        target: Value,
485        handler: Value,
486        revoked: bool,
487    },
488}
489
490/// Which variant a heap object is, carrying none of its contents.
491///
492/// Property access has to pick a branch by variant, but the code inside a branch
493/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
494/// hold a `&JsObj` borrow across the match. The way out used to be
495/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
496/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
497/// property read O(len) and any loop over a collection O(n^2). This type is the
498/// same discriminant with nothing attached, so the probe is O(1) and each branch
499/// re-borrows for only the one field it actually needs.
500/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
501/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
502/// under the sentinel string `@@<name>` (`property_key`) so the internal
503/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
504/// table walk. Symbols V8 defines but node-js does not act on are deliberately
505/// absent: a symbol that reads back while the operator it names ignores it would
506/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
507pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
508    "iterator",
509    "asyncIterator",
510    "toPrimitive",
511    "toStringTag",
512    "hasInstance",
513    // Nine more the table was missing entirely, so `Symbol.species` and friends
514    // read `undefined` and no protocol keyed on them could be expressed.
515    "species",
516    "isConcatSpreadable",
517    "match",
518    "matchAll",
519    "replace",
520    "search",
521    "split",
522    "unscopables",
523    "dispose",
524    "asyncDispose",
525];
526
527/// Whether the internal key `k` came from a SYMBOL used as a property key
528/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
529/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
530/// is an observable JavaScript property.
531pub fn is_symbol_key(k: &str) -> bool {
532    match k.strip_prefix("@@") {
533        Some(rest) => rest
534            .strip_prefix("sym:")
535            .map(|i| i.parse::<u64>().is_ok())
536            .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
537        None => false,
538    }
539}
540
541#[derive(Clone, Copy, PartialEq, Eq, Debug)]
542pub enum ObjKind {
543    Str,
544    Array,
545    Object,
546    Func,
547    Builtin,
548    BoundMethod,
549    Null,
550    Iter,
551    BoundFunc,
552    Class,
553    Symbol,
554    Map,
555    Set,
556    Generator,
557    Promise,
558    BigInt,
559    RegExp,
560    Proxy,
561}
562
563impl JsObj {
564    /// This object's variant, without touching its contents.
565    pub fn kind(&self) -> ObjKind {
566        match self {
567            JsObj::Str(_) => ObjKind::Str,
568            JsObj::Array(_) => ObjKind::Array,
569            JsObj::Object(_) => ObjKind::Object,
570            JsObj::Func(_) => ObjKind::Func,
571            JsObj::Builtin(_) => ObjKind::Builtin,
572            JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
573            JsObj::Null => ObjKind::Null,
574            JsObj::Iter { .. } => ObjKind::Iter,
575            JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
576            JsObj::Class(_) => ObjKind::Class,
577            JsObj::Symbol { .. } => ObjKind::Symbol,
578            JsObj::Map { .. } => ObjKind::Map,
579            JsObj::Set { .. } => ObjKind::Set,
580            JsObj::Generator { .. } => ObjKind::Generator,
581            JsObj::Promise { .. } => ObjKind::Promise,
582            JsObj::BigInt(_) => ObjKind::BigInt,
583            JsObj::RegExp(_) => ObjKind::RegExp,
584            JsObj::Proxy { .. } => ObjKind::Proxy,
585        }
586    }
587}
588
589/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
590/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
591/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
592/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
593#[derive(Clone)]
594pub struct RegExpObj {
595    /// The translated regex. Construction of a pattern fancy-regex still cannot
596    /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
597    /// `RegExpObj` always holds a compiled engine.
598    ///
599    /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
600    /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
601    /// per-object mutable state — while the compiled engine behind it is
602    /// immutable and identical every time. See `regexp::compiled`.
603    pub re: std::rc::Rc<fancy_regex::Regex>,
604    pub source: String,
605    pub flags: String,
606    pub global: bool,
607    pub ignore_case: bool,
608    pub multiline: bool,
609    pub dot_all: bool,
610    pub sticky: bool,
611    pub unicode: bool,
612    /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
613    /// `g`/`y` flags. The newtype keeps it from being confused with the regex
614    /// engine's byte offsets, which are the same shape and differ off the BMP.
615    pub last_index: crate::utf16::U16Index,
616}
617
618/// A Promise's settled state and pending reactions.
619pub struct PromiseCell {
620    pub state: PromiseState,
621    pub value: Value,
622    /// Reactions registered while still pending; drained (as microtasks) on
623    /// settle.
624    pub reactions: Vec<PromiseReaction>,
625    /// True once a rejection has been observed by a handler (`.then`/`.catch`),
626    /// so the loop doesn't report it as unhandled.
627    pub handled: bool,
628}
629
630/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
631/// a native continuation (Promise chaining / async `await` resumption).
632pub enum PromiseReaction {
633    Js {
634        on_ful: Value,
635        on_rej: Value,
636        result: Value,
637    },
638    Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
639}
640
641#[derive(Default, Clone, Copy, PartialEq, Eq)]
642pub enum PromiseState {
643    #[default]
644    Pending,
645    Fulfilled,
646    Rejected,
647}
648
649/// A live class constructor. The prototype object (holding instance methods) and
650/// the static-side own properties live on the heap; `parent` is the superclass
651/// constructor value (`None` for a base class).
652#[derive(Clone)]
653pub struct ClassVal {
654    pub name: String,
655    /// The constructor function value (a `JsObj::Func`), or `None` for a class
656    /// with only a synthesized default constructor.
657    pub ctor: Option<Value>,
658    pub parent: Option<Value>,
659    /// `C.prototype` — the object instances delegate to.
660    pub proto: Value,
661    /// Static own properties (static methods/fields), plus `name`/`prototype`.
662    pub statics: IndexMap<String, Value>,
663    /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
664    /// per-instance after `super()` (or at construction start for a base class).
665    /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
666    /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
667    /// result — it cannot be re-derived at run time (a field initialised from an
668    /// already-anonymous function held elsewhere must not be renamed).
669    pub fields: Vec<(String, Value, bool)>,
670    /// The FuncDef holding the class's source span (`String(C)`).
671    pub source_def: Option<usize>,
672}
673
674/// The result of resolving `super.name`: a getter to invoke (accessor property)
675/// or a directly-usable value (method / data property).
676pub enum SuperRef {
677    Getter(Value),
678    Data(Value),
679}
680
681/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
682/// `+0` are the same key, primitives compare by value, objects by heap identity.
683#[derive(Clone, PartialEq, Eq, Hash)]
684pub enum MapKey {
685    Undef,
686    Null,
687    Bool(bool),
688    /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
689    Num(u64),
690    /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
691    Big(String),
692    Str(String),
693    /// Heap identity (objects, arrays, functions, symbols).
694    Ref(u32),
695    /// A builtin intrinsic, by the name it answers to. Every bare reference
696    /// to `Math` or `parseInt` allocates a fresh handle, so heap identity
697    /// would make `new Set([Math, Math])` two entries; `strict_eq` compares
698    /// these by name too.
699    Intrinsic(String),
700}
701
702// ── environments ─────────────────────────────────────────────────────────────
703
704/// The map behind a scope. Hashing these with `FxHash` instead of the default
705/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
706/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
707pub type VarMap = IndexMap<String, Value>;
708
709/// A local-variable environment, shared (by `Rc`) between a frame and any nested
710/// function that captures it.
711pub struct EnvData {
712    pub vars: VarMap,
713    /// The names in `vars` that were declared `const`, so an assignment to one
714    /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
715    ///
716    /// A separate set rather than a flag inside `VarMap`'s value, because
717    /// `set_name` is a hot path — the common case is an env with NO consts,
718    /// where `is_empty()` settles it without hashing the name a second time.
719    pub consts: rustc_hash::FxHashSet<String>,
720    pub parent: Option<Env>,
721}
722pub type Env = Rc<RefCell<EnvData>>;
723
724/// An accessor property: `(getter, setter)`, either optional.
725pub type Accessor = (Option<Value>, Option<Value>);
726
727/// Prefix of the hidden property-map entry that reserves an accessor's slot in
728/// own-key insertion order (see `set_accessor`).
729pub const ORD_MARKER: &str = "@@ord:";
730
731/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
732/// all-true shape a plain `o.k = v` assignment produces, which is why only
733/// deviations need storing.
734#[derive(Clone, Copy, Debug, PartialEq, Eq)]
735pub struct PropAttrs {
736    pub writable: bool,
737    pub enumerable: bool,
738    pub configurable: bool,
739}
740
741impl Default for PropAttrs {
742    fn default() -> Self {
743        PropAttrs {
744            writable: true,
745            enumerable: true,
746            configurable: true,
747        }
748    }
749}
750
751impl PropAttrs {
752    /// The attribute shape V8 gives an internal-but-inspectable slot such as
753    /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
754    /// readable and replaceable, but never enumerated.
755    pub const HIDDEN: PropAttrs = PropAttrs {
756        writable: true,
757        enumerable: false,
758        configurable: true,
759    };
760}
761
762fn new_env(parent: Option<Env>) -> Env {
763    Rc::new(RefCell::new(EnvData {
764        vars: VarMap::default(),
765        consts: rustc_hash::FxHashSet::default(),
766        parent,
767    }))
768}
769
770/// A fresh empty scope chained under `parent`.
771pub fn child_env(parent: Env) -> Env {
772    new_env(Some(parent))
773}
774
775/// One function activation.
776pub struct Frame {
777    pub env: Env,
778    /// The env this activation started in — the FUNCTION scope. `var` and hoisted
779    /// function declarations bind here no matter how many block scopes are open.
780    pub base_env: Env,
781    pub this_obj: Option<Value>,
782    /// `new.target` for this activation (the constructor when invoked via `new`).
783    pub new_target: Option<Value>,
784    /// The class value owning the running method (drives `super`); `None` outside
785    /// a class method/constructor.
786    pub home_class: Option<Value>,
787    /// Whether the running method is a static one — see `FuncVal::home_static`.
788    pub home_static: bool,
789    /// The object literal owning the running method — see
790    /// `FuncVal::home_object`.
791    pub home_object: Option<Value>,
792    /// Whether the code in this activation is strict. A write the object
793    /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
794    /// ASSIGNMENT SITE decides — not the object being written to.
795    pub strict: bool,
796    /// Source line the frame is currently executing (updated by the DAP line hook
797    /// under `--dap`; stays 0 on ordinary runs).
798    pub line: u32,
799    /// The function name that owns this frame, for the DAP `stackTrace`; `None`
800    /// for the module frame and anonymous activations.
801    pub owner: Option<String>,
802    /// True ONLY for the program's module frame. A generator/async body runs on a
803    /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
804    /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
805    /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
806    /// across concurrent activations of the same function.
807    pub is_module: bool,
808    /// Whether this activation's `this` is bound yet — see [`ThisState`].
809    pub this_state: ThisState,
810}
811
812/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
813/// is observable: only a DERIVED class constructor starts with `this`
814/// uninitialized, and only `super()` binds it.
815///
816/// The instance is still allocated up front (`construct_class`), so the
817/// state is what makes it unreachable until then: `this` before `super()`, a
818/// second `super()`, and returning without one are each the error node raises
819/// rather than a silent write to the pre-allocated object.
820#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
821pub enum ThisState {
822    /// Every other activation: `this` is whatever was passed in.
823    #[default]
824    Plain,
825    /// A derived constructor before `super()` has returned.
826    Pending,
827    /// A derived constructor after `super()`.
828    Bound,
829}
830
831/// A non-local control signal. `Break`/`Continue` carry the optional loop label
832/// and are only raised when the target loop lives in an ENCLOSING chunk (a
833/// `break` inside a `try` block, which the host runs as its own chunk); a
834/// same-chunk `break` is a plain compiler-resolved jump.
835#[derive(Clone)]
836pub enum Signal {
837    Return(Value),
838    Break(Option<String>),
839    Continue(Option<String>),
840}
841
842/// The JavaScript runtime.
843pub struct JsHost {
844    heap: Vec<JsObj>,
845    /// Function templates, indexed by def id.
846    pub funcs: Vec<FuncDef>,
847    /// Every script text a loaded program was parsed from; a `FuncDef`'s
848    /// `script` indexes it and its `span` slices it.
849    pub scripts: Vec<std::sync::Arc<str>>,
850    /// try/catch/finally block templates, indexed by try id.
851    pub tries: Vec<TryDef>,
852    /// Module-level (global) names.
853    globals: VarMap,
854    /// The one uninitialized-binding marker, allocated on first use. See
855    /// [`JsHost::tdz_marker`].
856    tdz: Option<Value>,
857    /// Module-top-level names still in their temporal dead zone. Kept out of
858    /// `globals` so the marker is never reachable as `globalThis.<name>`.
859    tdz_globals: rustc_hash::FxHashSet<String>,
860    /// Top-level `const` names (a module frame declares into `globals`), so an
861    /// assignment to one throws the same way a block-scoped `const` does.
862    global_consts: rustc_hash::FxHashSet<String>,
863    /// The frame stack (bottom = module).
864    frames: Vec<Frame>,
865    /// The program's top-level scope — the scope runtime-compiled source runs in
866    /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
867    /// `run_chunk_in_global_scope`), as opposed to whatever function frame
868    /// happens to be executing when that source is compiled.
869    ///
870    /// Held as its own field rather than read off `frames[0]` because a coroutine
871    /// body runs with `frames` SWAPPED for its own one-frame context
872    /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
873    ///
874    /// Note this is node-js's ONE top-level scope. Node distinguishes the global
875    /// scope from a CommonJS module's scope (a module body is a wrapper
876    /// function), so in Node a file's top-level `var` is invisible to dynamic
877    /// code; here the entry file is evaluated with Script semantics, so it stays
878    /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
879    /// records for top-level `return`, not a separate one — and `node -e`, which
880    /// really is a Script, matches Node exactly.
881    global_env: Env,
882    pub error: Option<String>,
883    /// The in-flight thrown value, if any (JS `throw`).
884    pub exc: Option<Value>,
885    pub signal: Option<Signal>,
886    /// Promises that settled REJECTED this tick. Drained at each microtask
887    /// checkpoint: any still without a handler is an unhandled rejection.
888    pub pending_rejections: Vec<u32>,
889    /// `process.on(event, fn)` listeners, by event name.
890    pub process_listeners: IndexMap<String, Vec<ProcListener>>,
891    /// The canonical `null` handle (allocated once).
892    null_val: Value,
893    /// `[[Prototype]]` link per heap object, by heap index. Absent = default
894    /// (`Object.prototype` for objects, `null` for the root).
895    protos: HashMap<u32, Value>,
896    /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
897    /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
898    /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
899    /// is why `Object.create(null) instanceof Object` can read `false`.
900    null_proto_objs: HashSet<u32>,
901    /// Own properties of function objects (functions are objects in JS): a live
902    /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
903    fn_props: HashMap<u32, IndexMap<String, Value>>,
904    /// Accessor (getter/setter) properties per owning object, by heap index then
905    /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
906    accessors: HashMap<u32, IndexMap<String, Accessor>>,
907    /// Own-property attributes that deviate from the plain-assignment default
908    /// (`{writable, enumerable, configurable}` all true), by heap index then key.
909    /// Only non-default entries are stored, so an ordinary object costs nothing;
910    /// `prop_attrs` returns the default for any key absent here. This is what
911    /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
912    /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
913    /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
914    prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
915    /// Heap objects sealed against new properties by `Object.preventExtensions`,
916    /// `Object.seal` or `Object.freeze`.
917    non_extensible: HashSet<u32>,
918    /// Private names (`#m`) declared as a METHOD or accessor rather than as a
919    /// field, for the brand-check error text: node distinguishes `Receiver must
920    /// be an instance of class C` (a private method or accessor) from `Cannot
921    /// read private member #x …` (a private field). Which class is answered by
922    /// the running method's home class, not by this set, so two classes
923    /// declaring the same private method name stay exact.
924    private_methods: HashSet<String>,
925    /// The ELIDED element positions of each array, by heap index. Absent (the
926    /// overwhelmingly common case) means the array is dense.
927    ///
928    /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
929    /// to be mapped back to `undefined` at every element read in the runtime, and
930    /// a single missed read would leak an un-nameable value into user code — a
931    /// worse failure than storing `undefined` and losing the distinction. Keeping
932    /// the marker OUTSIDE the value domain makes that leak structurally
933    /// impossible: the element vector still holds a perfectly ordinary
934    /// `Value::Undef` at a hole, so any code path that has not been taught about
935    /// holes degrades to exactly the pre-existing behaviour (a visible
936    /// `undefined`) instead of producing something unrepresentable.
937    ///
938    /// Sized like the array it describes in the worst case (`new Array(n)` marks
939    /// every index), which is the same order as the `Vec<Value>` already paid for
940    /// that array — so it cannot turn a working allocation into an OOM.
941    array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
942    /// See `take_super_replacement`.
943    super_replacement: Option<Value>,
944    /// Set by `run_class_ctor` for the one call that follows: the next user
945    /// function activation is a derived constructor and starts `Pending`.
946    derived_ctor_next: bool,
947    /// Whether the entry script's top-level `var`s bind to its own scope rather
948    /// than to the globals map — the CommonJS wrapper Node puts every file in.
949    module_scope: bool,
950    /// User-assigned static properties on a builtin namespace/constructor, keyed
951    /// by namespace name then property (`Error` → `prepareStackTrace`,
952    /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
953    /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
954    /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
955    builtin_statics: HashMap<String, IndexMap<String, Value>>,
956    /// The shared well-known `Object.prototype` object (chain root for objects).
957    object_proto: Value,
958    /// Class name of each class `prototype` object, by heap index — lets an
959    /// instance recover its constructor name (for `util.inspect` prefix and
960    /// `obj.constructor.name`).
961    proto_class: HashMap<u32, Value>,
962    /// Class constructor values by name, so a running method's `home_class` name
963    /// resolves to its class value (for `super`).
964    class_registry: HashMap<String, Value>,
965    /// Well-known prototype objects for the builtin error constructors, by name.
966    error_protos: HashMap<String, Value>,
967    /// The template object of each tagged-template SITE, keyed by the chunk that
968    /// holds the site and the site's ordinal within its compilation.
969    ///
970    /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
971    /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
972    /// `t() === t()` true, and a tag that memoizes on the strings array — the
973    /// documented reason the object is cached, and how `lit-html` and `graphql`
974    /// avoid re-parsing — saw a fresh array every call here. Two sites with
975    /// identical text are still distinct objects, which the chunk hash plus the
976    /// ordinal keep apart.
977    template_objects: HashMap<(u64, u64), Value>,
978    /// Real prototype *objects* for the builtin exotics whose instances need a
979    /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
980    /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
981    /// cannot appear on a prototype chain and report `typeof "function"`.
982    native_protos: HashMap<String, Value>,
983    /// `Symbol.for` registry: description → symbol value.
984    symbol_registry: HashMap<String, Value>,
985    /// Monotonic id source for fresh `Symbol()` values.
986    next_symbol: u64,
987    /// Every live symbol by its id, so a `@@sym:<id>` property key can be
988    /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
989    symbols_by_id: HashMap<u64, Value>,
990    /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
991    /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
992    /// is a distinct key.
993    well_known_ids: HashMap<u64, String>,
994    /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
995    generators: Vec<GenCell>,
996    /// Promise cells, indexed by `JsObj::Promise.id`.
997    promises: Vec<PromiseCell>,
998    /// Whether the loop is part-way through draining the microtask queue, so a
999    /// `nextTick` queued by one of them waits for the round to finish. See
1000    /// `next_microtask`.
1001    draining_micro: bool,
1002    /// `process.nextTick` callbacks (drained before promise microtasks).
1003    pub nextticks: std::collections::VecDeque<Task>,
1004    /// Promise-reaction / `queueMicrotask` microtasks.
1005    pub microtasks: std::collections::VecDeque<Task>,
1006    /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
1007    pub macrotasks: Vec<Timer>,
1008    /// Monotonic timer-id source.
1009    next_timer: u64,
1010    /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
1011    /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
1012    /// sees a spurious `Disconnected` while a server is running.
1013    io_tx: Sender<IoTask>,
1014    /// Owned by the event loop (taken out for the blocking `recv`). Receives the
1015    /// `IoTask`s posted by I/O threads.
1016    io_rx: Option<Receiver<IoTask>>,
1017    /// Ref-count of "things keeping the process alive": open listeners, live
1018    /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1019    /// queues are empty. A pure script never touches it, so it exits exactly as
1020    /// before.
1021    open_handles: usize,
1022    /// In-process output sink. When `Some`, everything the program writes to
1023    /// stdout/stderr is appended here instead of reaching the process streams —
1024    /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1025    /// cannot corrupt its display. `None` (the default) is the ordinary
1026    /// standalone `node` behaviour: writes go straight to the real streams.
1027    ///
1028    /// Bytes, not `String`: a program may legitimately write output that is not
1029    /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1030    /// buffer can only hold the lossy `U+FFFD` transcription of it.
1031    capture: Option<Vec<u8>>,
1032    /// `process.exitCode`: the code the process exits with when the event loop
1033    /// drains, or `None` while unset. Separate from an explicit
1034    /// `process.exit(n)`, which exits immediately with `n`.
1035    pub exit_code: Option<i32>,
1036    /// Whether the `exit` event has already been emitted, so the `process.exit`
1037    /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1038    pub exiting: bool,
1039    /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1040    /// identity in JS, so `globalThis === globalThis` is `true` and a property
1041    /// written through one read is visible through the next. Minting a fresh
1042    /// object per read made both false.
1043    global_obj: Value,
1044}
1045
1046/// One `process.on`/`process.once` registration. `once` is not decoration: a
1047/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1048/// the same event does not reach it. Treating `once` as an alias of `on` made
1049/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1050/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1051/// reports zero listeners afterwards.
1052#[derive(Clone)]
1053pub struct ProcListener {
1054    pub f: Value,
1055    pub once: bool,
1056}
1057
1058/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1059/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1060pub enum Task {
1061    Js { cb: Value, args: Vec<Value> },
1062    Native(Box<dyn FnOnce() -> Result<(), String>>),
1063}
1064
1065impl Task {
1066    fn run(self) -> Result<(), String> {
1067        match self {
1068            Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1069            Task::Native(f) => f(),
1070        }
1071    }
1072}
1073
1074/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1075/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1076pub struct Timer {
1077    pub id: u64,
1078    pub delay: f64,
1079    pub seq: u64,
1080    pub callback: Value,
1081    pub args: Vec<Value>,
1082    pub cancelled: bool,
1083    /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1084    /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1085    /// deadline each time it fires, so it keeps the loop alive indefinitely —
1086    /// exactly like Node, where `setInterval` runs until cleared.
1087    pub interval: Option<f64>,
1088    /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1089    /// the event loop alive; an unref'd one still fires while the loop happens
1090    /// to be alive for another reason, but never holds it open by itself.
1091    pub refed: bool,
1092    /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1093    /// path (an open handle or a pending interval). On the pure virtual clock
1094    /// this is ignored.
1095    pub deadline: Instant,
1096}
1097
1098/// One suspended generator. `coro` is `None` only while actively running (taken
1099/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1100/// (frames/signal/error/exc) while suspended.
1101struct GenCell {
1102    coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1103    /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1104    /// thread → valid for the body's life). Read by `yield` to suspend.
1105    yielder: *const (),
1106    ctx: GenContext,
1107    done: bool,
1108    /// True once the body has been resumed at least once (so it is suspended at a
1109    /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1110    started: bool,
1111    /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1112    /// `yield` resume so the body unwinds (running any pending `finally`).
1113    inject: Option<GenInject>,
1114    /// True for an `async function*` body, where `await` AND `yield` share one
1115    /// coroutine yielder: `await` wraps its operand in an await marker so the
1116    /// driver can tell an internal suspension from a real yield.
1117    async_gen: bool,
1118    /// `[[AsyncGeneratorQueue]]` — pending requests as
1119    /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1120    /// overlapping requests resume the body ONE AT A TIME and settle in request
1121    /// order; without it a second request issued before the first settles races
1122    /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1123    /// all enqueue — a `.return()` that skipped the queue would terminate the
1124    /// body while an earlier `.next()` was still suspended on an `await`, and
1125    /// that `.next()` would then wrongly report `{done: true}`.
1126    queue: std::collections::VecDeque<(GenReq, u32)>,
1127    /// True while a queued request is being driven.
1128    running: bool,
1129    /// The [`stack_floor`] that applies while this generator's body is running.
1130    ///
1131    /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1132    /// range the thread's pthread record describes says nothing about how much
1133    /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1134    /// construction and swapped in around every resume; without it the guard
1135    /// compared a coroutine stack pointer against the main stack's floor and
1136    /// (depending on where mmap landed) either fired immediately or never.
1137    stack_floor: usize,
1138}
1139
1140/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1141enum GenInject {
1142    Return(Value),
1143    Throw(Value),
1144}
1145
1146/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1147/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1148/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1149/// instead of unwinding the body on the spot.
1150#[derive(Clone)]
1151pub enum GenReq {
1152    /// `.next(v)` — resume normally with `v`.
1153    Next(Value),
1154    /// `.return(v)` — resume with a forced return completion.
1155    Return(Value),
1156    /// `.throw(e)` — resume with a forced throw completion.
1157    Throw(Value),
1158}
1159
1160/// The mutable "execution registers" swapped at every generator resume/suspend
1161/// boundary so a suspended generator's half-finished frame/signal state never
1162/// leaks into the resuming caller. The heap, function/class tables and globals
1163/// are shared and never swapped.
1164#[derive(Default)]
1165struct GenContext {
1166    frames: Vec<Frame>,
1167    error: Option<String>,
1168    exc: Option<Value>,
1169    signal: Option<Signal>,
1170}
1171
1172thread_local! {
1173    /// Id of the generator whose body is currently executing, or `None` at the
1174    /// root. `yield` suspends this generator.
1175    static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1176}
1177
1178thread_local! {
1179    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1180}
1181
1182/// Run `f` with mutable access to the thread-local host.
1183pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1184    HOST.with(|h| f(&mut h.borrow_mut()))
1185}
1186
1187/// Reset the host to a clean slate (fresh module frame).
1188pub fn reset_host() {
1189    with_host(|h| *h = JsHost::new());
1190    // Drop any cached module handles / factory closure — they index the old heap.
1191    crate::module::reset();
1192}
1193
1194impl Default for JsHost {
1195    fn default() -> Self {
1196        Self::new()
1197    }
1198}
1199
1200impl JsHost {
1201    pub fn new() -> JsHost {
1202        let global_env = new_env(None);
1203        let (io_tx, io_rx) = std::sync::mpsc::channel();
1204        let mut h = JsHost {
1205            tdz: None,
1206            tdz_globals: Default::default(),
1207            heap: Vec::new(),
1208            funcs: Vec::new(),
1209            scripts: Vec::new(),
1210            tries: Vec::new(),
1211            globals: VarMap::default(),
1212            global_consts: rustc_hash::FxHashSet::default(),
1213            frames: vec![Frame {
1214                env: global_env.clone(),
1215                base_env: global_env.clone(),
1216                this_obj: None,
1217                new_target: None,
1218                home_class: None,
1219                home_static: false,
1220                home_object: None,
1221                strict: false,
1222                line: 0,
1223                owner: None,
1224                is_module: true,
1225                this_state: ThisState::Plain,
1226            }],
1227            global_env,
1228            error: None,
1229            exc: None,
1230            signal: None,
1231            pending_rejections: Vec::new(),
1232            process_listeners: IndexMap::new(),
1233            null_val: Value::Undef,
1234            protos: HashMap::new(),
1235            null_proto_objs: HashSet::new(),
1236            fn_props: HashMap::new(),
1237            accessors: HashMap::new(),
1238            prop_attrs: HashMap::new(),
1239            non_extensible: HashSet::new(),
1240            private_methods: HashSet::new(),
1241            array_holes: HashMap::new(),
1242            super_replacement: None,
1243            derived_ctor_next: false,
1244            module_scope: false,
1245            builtin_statics: HashMap::new(),
1246            object_proto: Value::Undef,
1247            proto_class: HashMap::new(),
1248            class_registry: HashMap::new(),
1249            error_protos: HashMap::new(),
1250            template_objects: HashMap::new(),
1251            native_protos: HashMap::new(),
1252            symbol_registry: HashMap::new(),
1253            next_symbol: 1,
1254            symbols_by_id: HashMap::new(),
1255            well_known_ids: HashMap::new(),
1256            generators: Vec::new(),
1257            promises: Vec::new(),
1258            microtasks: std::collections::VecDeque::new(),
1259            draining_micro: false,
1260            nextticks: std::collections::VecDeque::new(),
1261            macrotasks: Vec::new(),
1262            next_timer: 1,
1263            io_tx,
1264            io_rx: Some(io_rx),
1265            open_handles: 0,
1266            capture: None,
1267            exit_code: None,
1268            exiting: false,
1269            global_obj: Value::Undef,
1270        };
1271        h.null_val = h.alloc(JsObj::Null);
1272        // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1273        h.object_proto = h.new_object(IndexMap::new());
1274        h.global_obj = h.new_object(IndexMap::new());
1275        h
1276    }
1277
1278    /// Whether `v` IS the one `globalThis` object (not merely an object).
1279    pub fn is_global_object(&self, v: &Value) -> bool {
1280        !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1281    }
1282
1283    /// The `globalThis` object — one per host, so its identity and its
1284    /// properties both survive across reads.
1285    pub fn global_object(&mut self) -> Value {
1286        if matches!(self.global_obj, Value::Undef) {
1287            self.global_obj = self.new_object(IndexMap::new());
1288        }
1289        self.global_obj.clone()
1290    }
1291
1292    // ── prototype chain ──────────────────────────────────────────────────
1293    /// The `[[Prototype]]` of a heap value, if explicitly linked.
1294    pub fn proto_of(&self, v: &Value) -> Option<Value> {
1295        if let Value::Obj(i) = v {
1296            self.protos.get(i).cloned()
1297        } else {
1298            None
1299        }
1300    }
1301    /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1302    /// null-prototype object (recorded so `instanceof Object` reads false);
1303    /// undefined just clears any link without the null marker.
1304    pub fn set_proto(&mut self, v: &Value, proto: Value) {
1305        if let Value::Obj(i) = v {
1306            if self.is_null(&proto) {
1307                self.protos.remove(i);
1308                self.null_proto_objs.insert(*i);
1309            } else if matches!(proto, Value::Undef) {
1310                self.protos.remove(i);
1311            } else {
1312                self.protos.insert(*i, proto);
1313                self.null_proto_objs.remove(i);
1314            }
1315        }
1316    }
1317    /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1318    pub fn has_null_proto(&self, v: &Value) -> bool {
1319        matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1320    }
1321    /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1322    /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1323    /// for `Object.prototype` is null even though nothing ever set it so: it is
1324    /// the chain root and was never passed through `set_proto`, so the
1325    /// explicitly-nulled registry does not hold it and `console.log(Object
1326    /// .prototype)` printed a bare `{}` where node prints the tag.
1327    ///
1328    /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1329    /// about whether Object.prototype's own methods and `__proto__` accessor are
1330    /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1331    pub fn inspects_null_proto(&self, v: &Value) -> bool {
1332        self.has_null_proto(v) || *v == self.object_proto
1333    }
1334    pub fn object_proto(&self) -> Value {
1335        self.object_proto.clone()
1336    }
1337    /// Record that the prototype object `proto` belongs to the class constructor
1338    /// `class_val` (so instances can recover their constructor).
1339    pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1340        if let Value::Obj(i) = proto {
1341            self.proto_class.insert(*i, class_val);
1342        }
1343    }
1344    /// The class whose `prototype` object IS `v`, if `v` is one.
1345    pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1346        match v {
1347            Value::Obj(i) => self.proto_class.get(i).cloned(),
1348            _ => None,
1349        }
1350    }
1351    /// The class constructor value nearest in `obj`'s prototype chain, if any.
1352    pub fn class_of(&self, obj: &Value) -> Option<Value> {
1353        let mut cur = self.proto_of(obj);
1354        while let Some(p) = cur {
1355            if let Value::Obj(i) = &p {
1356                if let Some(c) = self.proto_class.get(i) {
1357                    return Some(c.clone());
1358                }
1359            }
1360            cur = self.proto_of(&p);
1361        }
1362        None
1363    }
1364    /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1365    /// object, no prefix).
1366    pub fn ctor_name(&self, obj: &Value) -> String {
1367        if let Some(c) = self.class_of(obj) {
1368            if let Some(JsObj::Class(cv)) = self.get(&c) {
1369                return cv.name.clone();
1370            }
1371        }
1372        // A `function F(){}` constructor is not a `class`, so it has no
1373        // `proto_class` entry. V8's `getConstructorName` walks the prototype
1374        // chain for an own `constructor` that is a named function — which is
1375        // what makes `console.log(new F())` print `F { y: 2 }`.
1376        let mut cur = self.proto_of(obj);
1377        while let Some(p) = cur {
1378            let ctor = match self.get(&p) {
1379                Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1380                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1381                _ => None,
1382            };
1383            if let Some(f) = ctor {
1384                let n = self.callable_name(&f);
1385                if !n.is_empty() {
1386                    return n;
1387                }
1388            }
1389            cur = self.proto_of(&p);
1390        }
1391        String::new()
1392    }
1393
1394    /// Whether a callable owns a `prototype` property. `MakeConstructor`
1395    /// (10.2.5) runs for an ordinary function definition and for every
1396    /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1397    /// function are not constructors and own none.
1398    pub fn owns_prototype(&self, v: &Value) -> bool {
1399        match self.get(v) {
1400            Some(JsObj::Class(_)) => true,
1401            Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1402                Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1403                None => false,
1404            },
1405            _ => false,
1406        }
1407    }
1408
1409    /// A function's own-property table (created on demand).
1410    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1411        if let Value::Obj(i) = v {
1412            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1413        } else {
1414            None
1415        }
1416    }
1417
1418    /// A class static member, inherited down the constructor chain: a subclass
1419    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1420    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1421        let mut cur = class_val.clone();
1422        loop {
1423            if let Some(v) = self.fn_prop(&cur, name) {
1424                return Some(v);
1425            }
1426            match self.get(&cur) {
1427                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1428                _ => return None,
1429            }
1430        }
1431    }
1432
1433    /// The first `extends` ancestor that is NOT a user class — the builtin
1434    /// constructor a class chain bottoms out in (`class D extends Array {}` →
1435    /// the `Array` builtin), or `None` for a chain of user classes only.
1436    ///
1437    /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1438    /// stops being a `Class`, so a static declared by the BUILTIN half of the
1439    /// chain was unreachable: `D.from` read `undefined` where node inherits
1440    /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1441    /// with an ordinary property read, which is what reaches a builtin's
1442    /// statics.
1443    pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1444        let mut cur = class_val.clone();
1445        loop {
1446            match self.get(&cur) {
1447                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1448                _ => return Some(cur),
1449            }
1450        }
1451    }
1452    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1453        if let Value::Obj(i) = v {
1454            self.fn_props
1455                .entry(*i)
1456                .or_default()
1457                .insert(name.to_string(), val);
1458        }
1459        // `name` and `prototype` are own properties of every function/class, but
1460        // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1461        // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1462        // script assigned. An ARRAY receiver reaching the same side table has no
1463        // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1464        if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1465            return;
1466        }
1467        let attrs = match name {
1468            "name" => PropAttrs {
1469                writable: false,
1470                enumerable: false,
1471                configurable: true,
1472            },
1473            "prototype" => PropAttrs {
1474                writable: true,
1475                enumerable: false,
1476                configurable: false,
1477            },
1478            _ => return,
1479        };
1480        self.set_prop_attrs(v, name, attrs);
1481    }
1482    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1483    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1484        self.builtin_statics
1485            .get(ns)
1486            .and_then(|m| m.get(name).cloned())
1487    }
1488    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1489    /// handles for the same namespace).
1490    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1491        self.builtin_statics
1492            .entry(ns.to_string())
1493            .or_default()
1494            .insert(name.to_string(), val);
1495    }
1496    /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1497    /// key was there — without this, `delete Array.prototype.patch` answered
1498    /// true and left the entry in place, so the patch outlived its own removal.
1499    pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1500        self.builtin_statics
1501            .get_mut(ns)
1502            .is_some_and(|m| m.shift_remove(name).is_some())
1503    }
1504    /// Every namespace a script has assigned a static onto, with that
1505    /// namespace's assigned keys — the source of the user-added half of
1506    /// `Object.getOwnPropertyNames(Array.prototype)`.
1507    pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1508        self.builtin_statics
1509            .get(ns)
1510            .map(|m| m.keys().cloned().collect())
1511            .unwrap_or_default()
1512    }
1513    /// Drop an own property from the side table (`delete arr.foo`,
1514    /// `delete fn.tag`). Reports whether the key was there.
1515    pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1516        match v {
1517            Value::Obj(i) => self
1518                .fn_props
1519                .get_mut(i)
1520                .map(|m| m.shift_remove(name).is_some())
1521                .unwrap_or(false),
1522            _ => false,
1523        }
1524    }
1525    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1526        if let Value::Obj(i) = v {
1527            self.fn_props
1528                .get(i)
1529                .map(|m| m.keys().cloned().collect())
1530                .unwrap_or_default()
1531        } else {
1532            Vec::new()
1533        }
1534    }
1535
1536    /// Install an accessor `(get, set)` for `key` on the object `owner`.
1537    pub fn set_accessor(
1538        &mut self,
1539        owner: &Value,
1540        key: &str,
1541        get: Option<Value>,
1542        set: Option<Value>,
1543    ) {
1544        if let Value::Obj(i) = owner {
1545            // Accessors live in their own table, but JS reports own keys in a
1546            // single insertion order across data AND accessor properties. Drop an
1547            // ordering marker into the property map so
1548            // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1549            // The marker is `@@`-prefixed, so it is invisible to every reader.
1550            let marker = format!("{ORD_MARKER}{key}");
1551            match self.get_mut(owner) {
1552                Some(JsObj::Object(props)) => {
1553                    if !props.contains_key(key) && !props.contains_key(&marker) {
1554                        props.insert(marker, Value::Undef);
1555                    }
1556                }
1557                // A function or class keeps its own properties in the fn-prop
1558                // side table, so its ordering marker belongs there. Without it a
1559                // static accessor enumerated AFTER every static field and method
1560                // regardless of where the class body declared it: node reports
1561                // `class A { static s = 2; static get sv(){} static m(){} }` as
1562                // `['sv', 'm', 's']` — the methods and accessors in source order
1563                // first, then the fields — and this reported `['m', 's', 'sv']`.
1564                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1565                    let table = self.fn_props.entry(*i).or_default();
1566                    if !table.contains_key(key) && !table.contains_key(&marker) {
1567                        table.insert(marker, Value::Undef);
1568                    }
1569                }
1570                _ => {}
1571            }
1572            let slot = self
1573                .accessors
1574                .entry(*i)
1575                .or_default()
1576                .entry(key.to_string())
1577                .or_insert((None, None));
1578            if get.is_some() {
1579                slot.0 = get;
1580            }
1581            if set.is_some() {
1582                slot.1 = set;
1583            }
1584        }
1585    }
1586    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1587    /// Drop an own accessor property entirely, marker and all.
1588    ///
1589    /// `delete obj.accessorProp` used to clear only the property map, and an
1590    /// accessor does not live there — so the delete reported success while the
1591    /// getter kept answering and `in` kept reporting the key.
1592    pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1593        if let Value::Obj(i) = owner {
1594            if let Some(m) = self.accessors.get_mut(i) {
1595                m.shift_remove(key);
1596            }
1597        }
1598        let marker = format!("{ORD_MARKER}{key}");
1599        match self.get_mut(owner) {
1600            Some(JsObj::Object(props)) => {
1601                props.shift_remove(&marker);
1602            }
1603            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1604                if let Value::Obj(i) = owner {
1605                    if let Some(t) = self.fn_props.get_mut(i) {
1606                        t.shift_remove(&marker);
1607                    }
1608                }
1609            }
1610            _ => {}
1611        }
1612    }
1613
1614    /// Turn an own accessor property into a data property carrying `value`,
1615    /// keeping its place in the own-key order.
1616    ///
1617    /// `set_accessor` records that order with an `@@ord:` marker in the
1618    /// property map rather than a real key, so deleting the accessor and
1619    /// inserting the value would append the key at the end instead. Node
1620    /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1621    /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1622    pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1623        if let Value::Obj(i) = owner {
1624            if let Some(m) = self.accessors.get_mut(i) {
1625                m.shift_remove(key);
1626            }
1627        }
1628        let marker = format!("{ORD_MARKER}{key}");
1629        let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1630            Some(pos) => {
1631                *map = map
1632                    .iter()
1633                    .enumerate()
1634                    .map(|(n, (k, v))| {
1635                        if n == pos {
1636                            (key.to_string(), value.clone())
1637                        } else {
1638                            (k.clone(), v.clone())
1639                        }
1640                    })
1641                    .collect();
1642            }
1643            None => {
1644                map.insert(key.to_string(), value.clone());
1645            }
1646        };
1647        let fn_table = matches!(
1648            self.get(owner),
1649            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1650        );
1651        if fn_table {
1652            if let Value::Obj(i) = owner {
1653                swap(self.fn_props.entry(*i).or_default());
1654            }
1655        } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1656            swap(props);
1657        }
1658    }
1659
1660    /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1661    ///
1662    /// Used when one object becomes another in place (a class extending a
1663    /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1664    /// already points at the leaf class's prototype, which is the one its
1665    /// methods must resolve through.
1666    pub fn move_index_state(&mut self, src: u32, dst: u32) {
1667        if let Some(holes) = self.array_holes.remove(&src) {
1668            self.array_holes.insert(dst, holes);
1669        }
1670        if let Some(attrs) = self.prop_attrs.remove(&src) {
1671            self.prop_attrs.entry(dst).or_default().extend(attrs);
1672        }
1673        if let Some(props) = self.fn_props.remove(&src) {
1674            self.fn_props.entry(dst).or_default().extend(props);
1675        }
1676        if let Some(acc) = self.accessors.remove(&src) {
1677            self.accessors.entry(dst).or_default().extend(acc);
1678        }
1679    }
1680
1681    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1682        if let Value::Obj(i) = owner {
1683            self.accessors.get(i).and_then(|m| m.get(key).cloned())
1684        } else {
1685            None
1686        }
1687    }
1688
1689    /// The own accessor-property keys of `owner`, in installation order.
1690    pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1691        match owner {
1692            Value::Obj(i) => self
1693                .accessors
1694                .get(i)
1695                .map(|m| m.keys().cloned().collect())
1696                .unwrap_or_default(),
1697            _ => Vec::new(),
1698        }
1699    }
1700
1701    // ── own-property attributes ──────────────────────────────────────────
1702
1703    /// Record non-default attributes for `owner[key]`. Storing the default shape
1704    /// clears the entry so the table only ever holds deviations.
1705    pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1706        if let Value::Obj(i) = owner {
1707            if attrs == PropAttrs::default() {
1708                if let Some(m) = self.prop_attrs.get_mut(i) {
1709                    m.shift_remove(key);
1710                }
1711            } else {
1712                self.prop_attrs
1713                    .entry(*i)
1714                    .or_default()
1715                    .insert(key.to_string(), attrs);
1716            }
1717        }
1718    }
1719
1720    /// Copy every recorded property attribute from `from` to `to`. A pass that
1721    /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1722    /// across or the copy silently re-exposes non-enumerable slots.
1723    pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1724        if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1725            if let Some(m) = self.prop_attrs.get(f).cloned() {
1726                for (k, a) in m {
1727                    self.set_prop_attrs(to, &k, a);
1728                }
1729            }
1730        }
1731    }
1732
1733    /// The attributes of own property `owner[key]` (all-true when unrecorded).
1734    pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1735        // An array's `length` is the array exotic's own property (10.4.2):
1736        // never enumerated and never configurable, and writable until
1737        // `Object.freeze` clears that — which is what stops a `push` from
1738        // extending a frozen array. Reporting it unconditionally writable made
1739        // `Object.isFrozen(Object.freeze([]))` false once the elements started
1740        // being sealed, because `length` was then the one key that never
1741        // followed.
1742        if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1743            let writable = match owner {
1744                Value::Obj(i) => self
1745                    .prop_attrs
1746                    .get(i)
1747                    .and_then(|m| m.get(key))
1748                    .map(|a| a.writable)
1749                    .unwrap_or(true),
1750                _ => true,
1751            };
1752            return PropAttrs {
1753                writable,
1754                enumerable: false,
1755                // An ARGUMENTS object's `length` is an ordinary data property
1756                // (10.4.4.6), so it is configurable where a real array's is
1757                // not. The two share a backing representation here, so the
1758                // exotic's attributes have to be told apart explicitly.
1759                configurable: crate::builtins::is_arguments_h(self, owner),
1760            };
1761        }
1762        match owner {
1763            Value::Obj(i) => self
1764                .prop_attrs
1765                .get(i)
1766                .and_then(|m| m.get(key))
1767                .copied()
1768                .unwrap_or_default(),
1769            _ => PropAttrs::default(),
1770        }
1771    }
1772
1773    /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1774    /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1775    pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1776        !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1777    }
1778
1779    /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1780    /// shape of every V8 "hidden but real" own property.
1781    pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1782        self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1783    }
1784
1785    /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1786    /// non-writable data property silently ignores the write in sloppy mode,
1787    /// which is the mode every script here runs in; so does adding a *new* key to
1788    /// a non-extensible object.
1789    pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1790        if !self.prop_attrs(owner, key).writable {
1791            return false;
1792        }
1793        // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1794        // and those members own no map entry for the walk below to find:
1795        // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1796        // is refused in node and was creating an own property here, which then
1797        // changed the object's brand.
1798        // The receiver's OWN kind counts too, not only the prototypes an
1799        // explicit link reaches: a plain array inherits `Array.prototype`
1800        // implicitly, with no link for the walk to follow, and
1801        // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1802        //
1803        // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1804        // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1805        // — are also OWN properties of every instance, so the inherited rule
1806        // never decides them; applying it anyway blocked `SetFunctionName`
1807        // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1808        // (x) {…}})` then threw.
1809        if key.starts_with("@@")
1810            && crate::builtins::own_ctor_name(self, owner)
1811                .into_iter()
1812                .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1813                .any(|c| crate::builtins::is_proto_readonly(c, key))
1814        {
1815            return false;
1816        }
1817        // 10.1.9.2: with no OWN property, the inherited one decides. A
1818        // non-writable data property up the chain blocks the write rather than
1819        // being shadowed — including one on a frozen prototype. Only own
1820        // attributes were consulted, so `Object.create(frozenBase).f = 2`
1821        // quietly created an own property node refuses to create.
1822        //
1823        // An inherited ACCESSOR does not block: its setter runs, and the write
1824        // path checks for one before reaching here.
1825        let has_own = match self.get(owner) {
1826            Some(JsObj::Object(p)) => p.contains_key(key),
1827            _ => true,
1828        };
1829        if !has_own {
1830            let mut cur = self.proto_of(owner);
1831            while let Some(proto) = cur {
1832                if self.own_accessor(&proto, key).is_some() {
1833                    break;
1834                }
1835                let present =
1836                    matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1837                if present {
1838                    if !self.prop_attrs(&proto, key).writable {
1839                        return false;
1840                    }
1841                    break;
1842                }
1843                cur = self.proto_of(&proto);
1844            }
1845        }
1846        if self.is_extensible(owner) {
1847            return true;
1848        }
1849        // A non-extensible object refuses a NEW key. Only the plain-object arm
1850        // could name its own keys, so every other shape answered "own" for any
1851        // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1852        // to the frozen template object a tagged template hands its tag.
1853        match self.get(owner) {
1854            Some(JsObj::Object(p)) => p.contains_key(key),
1855            Some(JsObj::Array(items)) => {
1856                key == "length"
1857                    || key
1858                        .parse::<usize>()
1859                        .map(|i| i < items.len())
1860                        .unwrap_or(false)
1861                    || self.fn_prop(owner, key).is_some()
1862            }
1863            // A RegExp's `lastIndex` is an own property, so a merely
1864            // NON-EXTENSIBLE regexp still accepts a write to it.
1865            Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1866            // Every other shape keeps its own properties in the fn-prop side
1867            // table (a function's statics, a Map's assigned properties), so
1868            // "does it already own this key" is that table's question. Answering
1869            // a blanket `true` let a NEW key land on a frozen function and a
1870            // frozen Map.
1871            _ => self.fn_prop(owner, key).is_some(),
1872        }
1873    }
1874
1875    /// Mark `v` closed to new properties (`Object.preventExtensions`).
1876    pub fn prevent_extensions(&mut self, v: &Value) {
1877        if let Value::Obj(i) = v {
1878            self.non_extensible.insert(*i);
1879        }
1880    }
1881
1882    pub fn is_extensible(&self, v: &Value) -> bool {
1883        !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1884    }
1885
1886    /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1887    /// the object and strip `configurable` — and, when freezing, `writable` —
1888    /// from every own property, data and accessor alike.
1889    pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1890        self.prevent_extensions(v);
1891        let mut keys = self.integrity_keys(v);
1892        keys.extend(self.own_accessor_keys(v));
1893        for k in keys {
1894            let mut a = self.prop_attrs(v, &k);
1895            a.configurable = false;
1896            if freeze {
1897                a.writable = false;
1898            }
1899            self.set_prop_attrs(v, &k, a);
1900        }
1901    }
1902
1903    /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1904    ///
1905    /// An array's elements are own properties too, and only the `Object` arm was
1906    /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1907    /// through, and the elements still reported `writable: true,
1908    /// configurable: true` while `Object.isFrozen` answered true over an empty
1909    /// key list. `length` is an own property as well, and freezing it is what
1910    /// stops a `push` from extending a frozen array.
1911    fn integrity_keys(&self, v: &Value) -> Vec<String> {
1912        let side_table_keys = |v: &Value| -> Vec<String> {
1913            match v {
1914                Value::Obj(i) => self
1915                    .fn_props
1916                    .get(i)
1917                    .map(|m| m.keys().cloned().collect())
1918                    .unwrap_or_default(),
1919                _ => Vec::new(),
1920            }
1921        };
1922        match self.get(v) {
1923            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1924            // A RegExp's only own property is its `lastIndex` cursor, which
1925            // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1926            // sealed nothing and a frozen regexp's cursor still moved.
1927            Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1928            // A function's statics and a Map's assigned properties live in the
1929            // fn-prop side table, and freezing has to reach them too.
1930            Some(JsObj::Func(_))
1931            | Some(JsObj::Class(_))
1932            | Some(JsObj::Map { .. })
1933            | Some(JsObj::Set { .. })
1934            | Some(JsObj::Promise { .. }) => side_table_keys(v),
1935            Some(JsObj::Array(items)) => (0..items.len())
1936                .map(|i| i.to_string())
1937                .chain(std::iter::once("length".to_string()))
1938                // A named property stuck on an array (`a.tag = 't'`, a match
1939                // array's `.index`/`.groups`) is an own property too, and
1940                // freezing has to reach it.
1941                .chain(match v {
1942                    Value::Obj(i) => self
1943                        .fn_props
1944                        .get(i)
1945                        .map(|m| m.keys().cloned().collect::<Vec<_>>())
1946                        .unwrap_or_default(),
1947                    _ => Vec::new(),
1948                })
1949                .collect(),
1950            _ => Vec::new(),
1951        }
1952    }
1953
1954    /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1955    pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1956        if self.is_extensible(v) {
1957            return false;
1958        }
1959        let mut keys = self.integrity_keys(v);
1960        keys.extend(self.own_accessor_keys(v));
1961        keys.iter().all(|k| {
1962            let a = self.prop_attrs(v, k);
1963            !a.configurable && (!freeze || !a.writable)
1964        })
1965    }
1966
1967    /// A fresh unique `Symbol(desc)` value.
1968    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1969        let id = self.next_symbol;
1970        self.next_symbol += 1;
1971        let v = self.alloc(JsObj::Symbol { desc, id });
1972        self.symbols_by_id.insert(id, v.clone());
1973        v
1974    }
1975
1976    /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1977    /// well-known `@@iterator`) came from.
1978    pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1979        if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1980            return self.symbols_by_id.get(&id).cloned();
1981        }
1982        let name = k.strip_prefix("@@")?;
1983        WELL_KNOWN_SYMBOLS
1984            .contains(&name)
1985            .then(|| {
1986                self.symbol_registry
1987                    .get(&format!("@@Symbol.{name}"))
1988                    .cloned()
1989            })
1990            .flatten()
1991    }
1992
1993    /// The own symbol-keyed property keys of `v` as SYMBOL values —
1994    /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
1995    pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
1996        let keys: Vec<String> = match self.get(v) {
1997            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1998            // An Array/Function receiver has no property map: its non-index own
1999            // properties — symbol-keyed ones included — live in the fn-prop side
2000            // table, and are just as much own properties as an object's.
2001            Some(_) => self.fn_prop_keys(v),
2002            None => return Vec::new(),
2003        };
2004        keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2005    }
2006
2007    /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2008    /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2009    /// alongside the string keys, and what `Object.keys` / `for-in` /
2010    /// `JSON.stringify` deliberately skip.
2011    pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2012        match self.get(v) {
2013            Some(JsObj::Object(p)) => p
2014                .iter()
2015                .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2016                .map(|(k, val)| (k.clone(), val.clone()))
2017                .collect(),
2018            // Array/Function: the side table (see `own_symbol_keys`).
2019            Some(_) => self
2020                .fn_prop_keys(v)
2021                .into_iter()
2022                .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2023                .map(|k| {
2024                    let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2025                    (k, val)
2026                })
2027                .collect(),
2028            None => Vec::new(),
2029        }
2030    }
2031    /// The shared `Symbol.for(key)` value (interned by description).
2032    pub fn symbol_for(&mut self, key: &str) -> Value {
2033        if let Some(v) = self.symbol_registry.get(key) {
2034            return v.clone();
2035        }
2036        let s = self.new_symbol(Some(key.to_string()));
2037        self.symbol_registry.insert(key.to_string(), s.clone());
2038        s
2039    }
2040    /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2041    /// or `undefined` for a symbol that is not in the registry at all.
2042    ///
2043    /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2044    /// `Symbol('k')` share a description and only the first is registered. The
2045    /// `@@Symbol.*` well-known entries are registry-internal and never a
2046    /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2047    /// `undefined` there.
2048    pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2049        let Some(key) = self
2050            .symbol_registry
2051            .iter()
2052            .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2053            .map(|(k, _)| k.clone())
2054        else {
2055            return Value::Undef;
2056        };
2057        self.new_str(key)
2058    }
2059    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2060    /// property key is `@@iterator`).
2061    pub fn well_known_iterator(&mut self) -> Value {
2062        self.symbol_for("@@Symbol.iterator")
2063    }
2064    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2065    pub fn well_known_async_iterator(&mut self) -> Value {
2066        self.symbol_for("@@Symbol.asyncIterator")
2067    }
2068    /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2069    /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2070    /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2071    ///
2072    /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2073    /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2074    /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2075    /// different symbol, and identification is by id, so a user-made
2076    /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2077    pub fn well_known_symbol(&mut self, name: &str) -> Value {
2078        let key = format!("@@Symbol.{name}");
2079        if let Some(v) = self.symbol_registry.get(&key) {
2080            return v.clone();
2081        }
2082        let s = self.new_symbol(Some(format!("Symbol.{name}")));
2083        if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2084            self.well_known_ids.insert(*id, name.to_string());
2085        }
2086        self.symbol_registry.insert(key, s.clone());
2087        s
2088    }
2089    /// The internal property-key string for a value used as a key. A `Symbol`
2090    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2091    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2092    pub fn property_key(&self, v: &Value) -> String {
2093        if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2094            if let Some(n) = self.well_known_ids.get(id) {
2095                return format!("@@{n}");
2096            }
2097            return format!("@@sym:{id}");
2098        }
2099        self.str_of(v)
2100    }
2101
2102    pub fn null(&self) -> Value {
2103        self.null_val.clone()
2104    }
2105    pub fn is_null(&self, v: &Value) -> bool {
2106        matches!(self.get(v), Some(JsObj::Null))
2107    }
2108
2109    // ── program loading ──────────────────────────────────────────────────
2110    pub fn program_offsets(&self) -> (usize, usize) {
2111        (self.funcs.len(), self.tries.len())
2112    }
2113    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2114        self.funcs.extend(funcs);
2115        self.tries.extend(tries);
2116    }
2117    /// The source text of function `def_id`, when its program kept one.
2118    pub fn func_source(&self, def_id: usize) -> Option<&str> {
2119        let d = self.funcs.get(def_id)?;
2120        let (start, end) = d.span;
2121        if end == 0 {
2122            return None;
2123        }
2124        self.scripts
2125            .get(d.script? as usize)?
2126            .get(start as usize..end as usize)
2127    }
2128    pub fn try_def(&self, id: usize) -> Option<TryDef> {
2129        self.tries.get(id).cloned()
2130    }
2131
2132    /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2133    /// finalizer)` — without copying its chunks. Running a `try` used to clone
2134    /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2135    /// handler and its finalizer on every iteration just to learn its shape.
2136    pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2137        let t = self.tries.get(id)?;
2138        Some((
2139            t.handler.is_some(),
2140            t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2141            t.finalizer.is_some(),
2142        ))
2143    }
2144
2145    /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2146    /// Reached only when no pooled VM already holds that chunk.
2147    pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2148        let t = self.tries.get(id)?;
2149        match part {
2150            0 => Some(t.block.clone()),
2151            1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2152            _ => t.finalizer.clone(),
2153        }
2154    }
2155
2156    // ── heap allocation / accessors ──────────────────────────────────────
2157    pub fn alloc(&mut self, obj: JsObj) -> Value {
2158        self.heap.push(obj);
2159        Value::Obj((self.heap.len() - 1) as u32)
2160    }
2161    pub fn get(&self, v: &Value) -> Option<&JsObj> {
2162        if let Value::Obj(i) = v {
2163            self.heap.get(*i as usize)
2164        } else {
2165            None
2166        }
2167    }
2168    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2169        if let Value::Obj(i) = v {
2170            self.heap.get_mut(*i as usize)
2171        } else {
2172            None
2173        }
2174    }
2175    /// Which variant `v` points at, without copying its contents. Use this in
2176    /// place of `get(v).cloned()` whenever only the tag is needed — see
2177    /// [`ObjKind`].
2178    pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2179        self.get(v).map(JsObj::kind)
2180    }
2181    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2182        self.alloc(JsObj::Str(s.into()))
2183    }
2184    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2185        self.alloc(JsObj::Array(items))
2186    }
2187
2188    /// Record that `name` was declared as a private method or accessor.
2189    pub fn note_private_method(&mut self, name: &str) {
2190        self.private_methods.insert(name.to_string());
2191    }
2192
2193    /// Whether `name` was declared as a private method/accessor by some class,
2194    /// as opposed to a private field.
2195    pub fn is_private_method(&self, name: &str) -> bool {
2196        self.private_methods.contains(name)
2197    }
2198
2199    /// The name of the class whose body the running function belongs to. Only a
2200    /// method of that class can even mention its private names, so this is the
2201    /// class a failed brand check must name.
2202    /// The `super` binding of the frame now running: the owning class name,
2203    /// whether the method is static, and the home object of an object-literal
2204    /// method. An ARROW captures all three at creation, the way it captures
2205    /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2206    /// Whether the activation now running is strict code.
2207    /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2208    /// with no `'use strict'` of its own or inherited from its script. This is
2209    /// the receiver test the `arguments`/`caller` poison pill keys on: node
2210    /// decides by the FUNCTION, never by the code doing the reading.
2211    pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2212        match self.get(v) {
2213            Some(JsObj::Func(fv)) => {
2214                !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2215            }
2216            _ => false,
2217        }
2218    }
2219    pub fn current_strict(&self) -> bool {
2220        self.frame().strict
2221    }
2222
2223    /// Mark the frame about to run as STRICT — used for a program whose own top
2224    /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2225    pub fn set_current_strict(&mut self) {
2226        if let Some(f) = self.frames.last_mut() {
2227            f.strict = true;
2228        }
2229    }
2230
2231    pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2232        (
2233            self.current_home_class_name(),
2234            self.frame().home_static,
2235            self.frame().home_object.clone(),
2236        )
2237    }
2238
2239    pub fn current_home_class_name(&self) -> Option<String> {
2240        match self.get(&self.current_home_class()?) {
2241            Some(JsObj::Class(c)) => Some(c.name.clone()),
2242            _ => None,
2243        }
2244    }
2245
2246    /// Whether `recv` — or anything on its prototype chain — carries the private
2247    /// name `key`. A private FIELD is an own property of the instance; a private
2248    /// METHOD lives on the class prototype, one link up.
2249    pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2250        let mut cur = Some(recv.clone());
2251        while let Some(v) = cur {
2252            let owns = match self.get(&v) {
2253                Some(JsObj::Object(p)) => p.contains_key(key),
2254                Some(JsObj::Class(c)) => c.statics.contains_key(key),
2255                _ => false,
2256            };
2257            if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2258                return true;
2259            }
2260            cur = self.proto_of(&v);
2261        }
2262        false
2263    }
2264
2265    // ── array holes ──────────────────────────────────────────────────────
2266    //
2267    // Every read/write of an array's elision set goes through this block. See
2268    // the `array_holes` field for why the marker lives here rather than in
2269    // `Value`.
2270
2271    /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2272    /// opposed to a stored `undefined`. `false` for anything that is not an
2273    /// array, and for every index of a dense one.
2274    pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2275        match (arr, ()) {
2276            (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2277            _ => false,
2278        }
2279    }
2280
2281    /// Whether `arr` has any elided element at all — one hash probe, and the
2282    /// guard every hole-aware code path takes before doing anything slower.
2283    pub fn has_holes(&self, arr: &Value) -> bool {
2284        matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2285    }
2286
2287    /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2288    /// Sorted because every consumer (own-key enumeration, `util.inspect`
2289    /// run-grouping) needs index order, and the backing set has none.
2290    pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2291        let Value::Obj(i) = arr else {
2292            return Vec::new();
2293        };
2294        let Some(hs) = self.array_holes.get(i) else {
2295            return Vec::new();
2296        };
2297        let mut v: Vec<usize> = hs.iter().copied().collect();
2298        v.sort_unstable();
2299        v
2300    }
2301
2302    /// Record element `i` of `arr` as elided.
2303    pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2304        if let Value::Obj(idx) = arr {
2305            self.array_holes.entry(*idx).or_default().insert(i);
2306        }
2307    }
2308
2309    /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2310    /// the gap a write past the end opens).
2311    pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2312        if range.is_empty() {
2313            return;
2314        }
2315        if let Value::Obj(idx) = arr {
2316            self.array_holes.entry(*idx).or_default().extend(range);
2317        }
2318    }
2319
2320    /// Element `i` now holds a real value: it is no longer a hole. Every write
2321    /// to an array index calls this, which is what keeps a stale hole record
2322    /// from outliving the elision it described.
2323    pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2324        let Value::Obj(idx) = arr else { return };
2325        let Some(hs) = self.array_holes.get_mut(idx) else {
2326            return;
2327        };
2328        hs.remove(&i);
2329        if hs.is_empty() {
2330            self.array_holes.remove(idx);
2331        }
2332    }
2333
2334    /// `arr` is dense from here on (`fill` over the whole array, a fresh
2335    /// dense assignment into an existing handle).
2336    pub fn clear_holes(&mut self, arr: &Value) {
2337        if let Value::Obj(idx) = arr {
2338            self.array_holes.remove(idx);
2339        }
2340    }
2341
2342    /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2343    /// `f`. Used by every method that derives a new array whose holes track the
2344    /// source's (`slice`, `concat`, `map`).
2345    pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2346        if !self.has_holes(src) {
2347            return;
2348        }
2349        let moved: rustc_hash::FxHashSet<usize> =
2350            self.hole_indices(src).into_iter().filter_map(f).collect();
2351        self.install_holes(dst, moved);
2352    }
2353
2354    /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2355    /// existing hole moves to, or `None` if the mutation removed it. This is the
2356    /// one primitive behind every structural array mutation — `shift` is
2357    /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2358    /// `splice` is the general case.
2359    pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2360        if !self.has_holes(arr) {
2361            return;
2362        }
2363        let moved: rustc_hash::FxHashSet<usize> =
2364            self.hole_indices(arr).into_iter().filter_map(f).collect();
2365        self.install_holes(arr, moved);
2366    }
2367
2368    /// Replace `arr`'s elision set outright, dropping the record entirely when
2369    /// the new set is empty so `has_holes` stays a single negative probe for the
2370    /// dense case.
2371    pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2372        let Value::Obj(idx) = arr else { return };
2373        if holes.is_empty() {
2374            self.array_holes.remove(idx);
2375        } else {
2376            self.array_holes.insert(*idx, holes);
2377        }
2378    }
2379
2380    /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2381    /// truncating `splice` leaves behind.
2382    pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2383        self.remap_holes(arr, |i| (i < len).then_some(i));
2384    }
2385
2386    /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2387    /// array, where each maximal run of elided positions collapses to a single
2388    /// `<N empty items>` entry. Returns the entries and whether the last of them
2389    /// is the `... N more items` tail (which the grid layout must not size a
2390    /// column to).
2391    ///
2392    /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2393    /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2394    /// sparse arrays print a single `<200 empty items>`.
2395    fn inspect_sparse(
2396        &self,
2397        v: &Value,
2398        items: &[Value],
2399        indent: usize,
2400        st: &mut InspectCycles,
2401    ) -> (Vec<String>, bool) {
2402        let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2403        let empties = |n: usize| {
2404            let unit = if n == 1 { "item" } else { "items" };
2405            format!("<{n} empty {unit}>")
2406        };
2407        let mut out: Vec<String> = Vec::new();
2408        // The first index not yet accounted for by an entry.
2409        let mut index = 0usize;
2410        for (i, it) in items.iter().enumerate() {
2411            if out.len() >= inspect_max_array_length() {
2412                break;
2413            }
2414            if holes.contains(&i) {
2415                continue;
2416            }
2417            if i > index {
2418                out.push(empties(i - index));
2419                index = i;
2420                if out.len() >= inspect_max_array_length() {
2421                    break;
2422                }
2423            }
2424            out.push(self.inspect_lvl(it, indent + 2, st));
2425            index = i + 1;
2426        }
2427        let remaining = items.len() - index;
2428        if remaining == 0 {
2429            return (out, false);
2430        }
2431        if out.len() < inspect_max_array_length() {
2432            // Trailing holes are still `<N empty items>`, not a truncation.
2433            out.push(empties(remaining));
2434            (out, false)
2435        } else {
2436            let unit = if remaining == 1 { "item" } else { "items" };
2437            out.push(format!("... {remaining} more {unit}"));
2438            (out, true)
2439        }
2440    }
2441    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2442        // Integer-index keys enumerate ascending-first regardless of the order
2443        // they were supplied in (object literal, spread, Object.assign result).
2444        canonicalize_own_keys(&mut props);
2445        // A map carrying the hidden `@@native` tag IS an instance of that native
2446        // class, so it hangs off the class prototype rather than
2447        // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2448        // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2449        // `AbortController` among them — built plain objects instead, so
2450        // `x.constructor.name` read `"Object"` and a chain walk found none of
2451        // the class's methods. Linking HERE means a construction site cannot
2452        // forget it; the tag is already in the map at every one of them.
2453        let tag = props.get("@@native").and_then(|v| self.as_str(v));
2454        let obj = self.alloc(JsObj::Object(props));
2455        if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2456            self.set_proto(&obj, proto);
2457        }
2458        obj
2459    }
2460    pub fn as_str(&self, v: &Value) -> Option<String> {
2461        match v {
2462            Value::Str(s) => Some((**s).clone()),
2463            Value::Obj(_) => match self.get(v) {
2464                Some(JsObj::Str(s)) => Some(s.clone()),
2465                _ => None,
2466            },
2467            _ => None,
2468        }
2469    }
2470
2471    // ── scope / names ────────────────────────────────────────────────────
2472    fn frame(&self) -> &Frame {
2473        self.frames.last().unwrap()
2474    }
2475    fn cur_env(&self) -> Env {
2476        self.frame().env.clone()
2477    }
2478
2479    // ── DAP debug introspection (used only under `--dap`) ────────────────────
2480    /// Number of active call frames (the debugger's step-depth reference).
2481    pub fn frame_depth(&self) -> usize {
2482        self.frames.len()
2483    }
2484    /// Record the source line the innermost frame is executing (DAP line hook).
2485    pub fn set_cur_line(&mut self, line: u32) {
2486        if let Some(f) = self.frames.last_mut() {
2487            f.line = line;
2488        }
2489    }
2490    /// The `.stack` tail for an error created right now: one `    at <name>`
2491    /// line per live frame, innermost first, ending at the module frame.
2492    ///
2493    /// These are the REAL user frames — node-js has no `file:line:column` (the
2494    /// per-frame line is only tracked under `--dap`) and no Node-internal
2495    /// module-loader frames, so `.stack` names the call chain but can never be
2496    /// byte-identical to V8's. The names are what makes a thrown error
2497    /// diagnosable; the missing positions are documented in BUGS.md.
2498    /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2499    ///
2500    /// The default is 10, it is settable, and setting it to 0 is the documented
2501    /// way to make error construction cheap. It did not exist, so the read was
2502    /// `undefined` and every stack carried every frame regardless.
2503    pub fn stack_trace_limit(&self) -> usize {
2504        match self.builtin_static("Error", "stackTraceLimit") {
2505            Some(v) => {
2506                let n = self.to_number(&v);
2507                if n.is_finite() && n > 0.0 {
2508                    n as usize
2509                } else if n.is_nan() || n <= 0.0 {
2510                    0
2511                } else {
2512                    usize::MAX
2513                }
2514            }
2515            None => 10,
2516        }
2517    }
2518
2519    pub fn stack_frames(&self) -> String {
2520        let limit = self.stack_trace_limit();
2521        if limit == 0 {
2522            return String::new();
2523        }
2524        let mut out = String::new();
2525        for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2526            let name = match (&f.owner, i) {
2527                (Some(n), _) => n.clone(),
2528                (None, 0) => "Object.<anonymous>".to_string(),
2529                (None, _) => "<anonymous>".to_string(),
2530            };
2531            out.push_str("\n    at ");
2532            out.push_str(&name);
2533        }
2534        if out.is_empty() && limit > 0 {
2535            out.push_str("\n    at <anonymous>");
2536        }
2537        out
2538    }
2539
2540    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2541    /// `stackTrace`. `owner` carries the function name where known.
2542    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2543        self.frames
2544            .iter()
2545            .rev()
2546            .map(|f| {
2547                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2548                (name, f.line)
2549            })
2550            .collect()
2551    }
2552    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2553    pub fn dbg_locals(&self) -> Vec<(String, String)> {
2554        let env = self.cur_env();
2555        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2556        names
2557            .into_iter()
2558            .map(|n| {
2559                let v = self.read_name(&n).unwrap_or(Value::Undef);
2560                (n, self.inspect(&v))
2561            })
2562            .collect()
2563    }
2564
2565    /// Scope-chain read: local + enclosing chain, then globals.
2566    /// Whether `name` is a module-top-level binding that has not reached its
2567    /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2568    /// block-scoped one by inspecting the value it holds.
2569    pub fn is_tdz_global(&self, name: &str) -> bool {
2570        self.tdz_globals.contains(name)
2571    }
2572
2573    pub fn read_name(&self, name: &str) -> Option<Value> {
2574        let mut env = Some(self.cur_env());
2575        while let Some(e) = env {
2576            if let Some(v) = e.borrow().vars.get(name) {
2577                return Some(v.clone());
2578            }
2579            env = e.borrow().parent.clone();
2580        }
2581        self.globals.get(name).cloned()
2582    }
2583    pub fn read_global(&self, name: &str) -> Option<Value> {
2584        self.globals.get(name).cloned()
2585    }
2586
2587    /// Whether `name` is bound anywhere on the scope chain or in the globals —
2588    /// `read_name(..).is_some()` without cloning the value it finds. The
2589    /// strict-mode assignment path asks this and nothing else.
2590    pub fn has_name(&self, name: &str) -> bool {
2591        let mut env = Some(self.cur_env());
2592        while let Some(e) = env {
2593            if e.borrow().vars.contains_key(name) {
2594                return true;
2595            }
2596            env = e.borrow().parent.clone();
2597        }
2598        self.globals.contains_key(name)
2599    }
2600
2601    /// Assign to an existing binding up the scope chain, else create a global
2602    /// (JS assignment to an undeclared name targets the global object).
2603    /// Assign to an existing binding, or create a global. Returns `false` when
2604    /// the nearest binding is an immutable (`const`) one, which the caller turns
2605    /// into `TypeError: Assignment to constant variable.` — assigning to a
2606    /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2607    /// a mutated constant.
2608    #[must_use]
2609    pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2610        let mut env = Some(self.cur_env());
2611        while let Some(e) = env {
2612            // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2613            // binding hashed the name twice and allocated a fresh `String` key
2614            // for a key that was already there — once per assignment, so once
2615            // per loop iteration in any counting loop.
2616            //
2617            // The const check runs only at the env that OWNS the name, and the
2618            // `is_empty` guard settles the common (no consts here) case without
2619            // hashing the name again.
2620            let mut b = e.borrow_mut();
2621            if b.vars.contains_key(name) {
2622                if !b.consts.is_empty() && b.consts.contains(name) {
2623                    return false;
2624                }
2625                if let Some(slot) = b.vars.get_mut(name) {
2626                    *slot = val;
2627                }
2628                return true;
2629            }
2630            drop(b);
2631            env = e.borrow().parent.clone();
2632        }
2633        if self.global_consts.contains(name) {
2634            return false;
2635        }
2636        match self.globals.get_mut(name) {
2637            Some(slot) => *slot = val,
2638            None => {
2639                self.globals.insert(name.to_string(), val);
2640            }
2641        }
2642        true
2643    }
2644
2645    /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2646    /// plus recording the name as immutable in whichever scope received it.
2647    pub fn declare_const_name(&mut self, name: &str, val: Value) {
2648        let f = self.frame();
2649        let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2650        self.declare_name(name, val);
2651        if to_globals {
2652            self.global_consts.insert(name.to_string());
2653        } else {
2654            self.cur_env().borrow_mut().consts.insert(name.to_string());
2655        }
2656    }
2657
2658    /// The value a lexical binding holds between entering its scope and reaching
2659    /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2660    /// process, so the check is a heap-index comparison and the marker cannot be
2661    /// produced by any JavaScript expression. It never escapes: every path that
2662    /// could read it throws first.
2663    pub fn tdz_marker(&mut self) -> Value {
2664        if let Some(v) = &self.tdz {
2665            return v.clone();
2666        }
2667        let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2668        self.tdz = Some(v.clone());
2669        v
2670    }
2671
2672    /// Whether `v` is the uninitialized-binding marker.
2673    pub fn is_tdz(&self, v: &Value) -> bool {
2674        matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2675    }
2676
2677    /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2678    /// already binds it. Emitted at the top of every scope for each `let`,
2679    /// `const` and `class` declared directly in it, so a read before the
2680    /// declaration throws instead of finding an OUTER binding of the same name —
2681    /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2682    pub fn hoist_tdz(&mut self, name: &str) {
2683        let marker = self.tdz_marker();
2684        let f = self.frame();
2685        // At module top level a lexical binding lives in `globals`, which is ALSO
2686        // what backs `globalThis.<name>` — so parking the marker there exposes it
2687        // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2688        // back as the marker. Top-level dead zones are tracked in a separate set
2689        // that only the name-read path consults.
2690        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2691            if !self.globals.contains_key(name) {
2692                self.tdz_globals.insert(name.to_string());
2693            }
2694            return;
2695        }
2696        let env = self.cur_env();
2697        let mut e = env.borrow_mut();
2698        if !e.vars.contains_key(name) {
2699            e.vars.insert(name.to_string(), marker);
2700        }
2701    }
2702
2703    /// Declare a new binding in the current scope (`let`/`const`). At the top of
2704    /// the module frame there is no local env, so those names become globals; once
2705    /// a block scope is open the binding belongs to that block.
2706    pub fn declare_name(&mut self, name: &str, val: Value) {
2707        let f = self.frame();
2708        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2709            self.tdz_globals.remove(name);
2710            self.globals.insert(name.to_string(), val);
2711        } else {
2712            self.cur_env()
2713                .borrow_mut()
2714                .vars
2715                .insert(name.to_string(), val);
2716        }
2717    }
2718
2719    /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2720    /// skips every open block scope and lands in the activation's base env.
2721    /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2722    /// name is not already bound in this activation.
2723    ///
2724    /// `var` bindings come into existence when the scope is entered, not where
2725    /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2726    /// rather than throwing. "If absent" is what keeps a parameter intact: in
2727    /// `function f(a) { var a; }` the `var` names a binding that already exists
2728    /// and must not be reset, which is also why a bare `var x;` emits nothing at
2729    /// its own position.
2730    pub fn hoist_var_name(&mut self, name: &str) {
2731        // The ENTRY script's top level is a CommonJS module body, not global
2732        // scope: node wraps every file in a function, so a top-level `var` is a
2733        // local of that wrapper. Binding it into the globals map made
2734        // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2735        // node says `undefined` — a REQUIRED module already ran inside a real
2736        // frame and behaved correctly, so only the entry file differed.
2737        if self.frame().is_module && !self.module_scope {
2738            self.globals.entry(name.to_string()).or_insert(Value::Undef);
2739            return;
2740        }
2741        let base = self.frame().base_env.clone();
2742        let mut env = base.borrow_mut();
2743        if !env.vars.contains_key(name) {
2744            env.vars.insert(name.to_string(), Value::Undef);
2745        }
2746    }
2747
2748    pub fn declare_var_name(&mut self, name: &str, val: Value) {
2749        if self.frame().is_module && !self.module_scope {
2750            self.globals.insert(name.to_string(), val);
2751            return;
2752        }
2753        let base = self.frame().base_env.clone();
2754        base.borrow_mut().vars.insert(name.to_string(), val);
2755    }
2756
2757    /// Enter a fresh block scope.
2758    pub fn push_scope(&mut self) {
2759        let env = self.cur_env();
2760        self.frames.last_mut().unwrap().env = child_env(env);
2761    }
2762
2763    /// Open a scope that is also the activation's VARIABLE environment, and
2764    /// return the previous one so the caller can restore it.
2765    ///
2766    /// A block scope is not enough for a strict direct `eval`: `var` and a
2767    /// hoisted function declaration bind to `base_env`, so they walked straight
2768    /// past a plain `push_scope` and still landed in the caller's function
2769    /// scope. Only `let`/`const` were contained.
2770    pub fn push_var_scope(&mut self) -> Env {
2771        let env = child_env(self.cur_env());
2772        let f = self.frames.last_mut().unwrap();
2773        let prev = std::mem::replace(&mut f.base_env, env.clone());
2774        f.env = env;
2775        prev
2776    }
2777
2778    /// Restore the variable environment a `push_var_scope` replaced.
2779    pub fn pop_var_scope(&mut self, prev: Env) {
2780        let f = self.frames.last_mut().unwrap();
2781        f.env = prev.clone();
2782        f.base_env = prev;
2783    }
2784
2785    /// Leave the innermost block scope (never pops past the activation's base).
2786    pub fn pop_scope(&mut self) {
2787        let cur = self.cur_env();
2788        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2789            return;
2790        }
2791        let parent = cur.borrow().parent.clone();
2792        if let Some(p) = parent {
2793            self.frames.last_mut().unwrap().env = p;
2794        }
2795    }
2796
2797    /// Replace the innermost block scope with a fresh copy of its bindings — the
2798    /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2799    /// in one iteration keeps that iteration's value.
2800    pub fn copy_scope(&mut self) {
2801        let cur = self.cur_env();
2802        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2803            return;
2804        }
2805        let parent = cur.borrow().parent.clone();
2806        let fresh = new_env(parent);
2807        fresh.borrow_mut().vars = cur.borrow().vars.clone();
2808        self.frames.last_mut().unwrap().env = fresh;
2809    }
2810
2811    /// The current block-scope env, for save/restore across a nested chunk.
2812    pub fn scope_snapshot(&self) -> Env {
2813        self.cur_env()
2814    }
2815    pub fn restore_scope(&mut self, env: Env) {
2816        self.frames.last_mut().unwrap().env = env;
2817    }
2818    pub fn set_global(&mut self, name: &str, val: Value) {
2819        self.globals.insert(name.to_string(), val);
2820    }
2821
2822    // ── output capture ───────────────────────────────────────────────────
2823    //
2824    // Every write a *program* makes — `console.log`, `process.stdout.write`,
2825    // `print` — funnels through `write_out`, so turning capture on redirects all
2826    // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2827    // crash traceback from `main`) deliberately do not: they belong to the
2828    // process, not to the program.
2829
2830    /// Start capturing program output in-process. Any text already captured is
2831    /// discarded, so each run starts clean.
2832    pub fn begin_capture(&mut self) {
2833        self.capture = Some(Vec::new());
2834    }
2835
2836    /// Stop capturing and take everything written since [`begin_capture`],
2837    /// returning the empty string when capture was not on. The captured bytes
2838    /// are rendered lossily: this API hands back a `String`, so a program that
2839    /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2840    /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2841    ///
2842    /// [`begin_capture`]: JsHost::begin_capture
2843    /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2844    pub fn end_capture(&mut self) -> String {
2845        String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2846    }
2847
2848    /// Stop capturing and take the raw bytes, without the lossy transcription
2849    /// [`end_capture`] applies.
2850    ///
2851    /// [`end_capture`]: JsHost::end_capture
2852    pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2853        self.capture.take().unwrap_or_default()
2854    }
2855
2856    /// Whether output is being captured — the one thing a caller needs to know
2857    /// before asking the real stream a question (`isTTY`, cursor position).
2858    pub fn capturing(&self) -> bool {
2859        self.capture.is_some()
2860    }
2861
2862    /// Write program output: into the capture buffer when capturing, else to the
2863    /// process stream `stderr` selects. `s` is written verbatim — callers add
2864    /// their own line ending, as `console.log` does and `process.stdout.write`
2865    /// does not.
2866    pub fn write_out(&mut self, s: &str, stderr: bool) {
2867        self.write_out_bytes(s.as_bytes(), stderr);
2868    }
2869
2870    /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2871    /// a byte string and Node writes it through untouched, so a `Buffer` holding
2872    /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2873    /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2874    /// became seven — so the byte path exists separately from [`write_out`].
2875    ///
2876    /// [`write_out`]: JsHost::write_out
2877    pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2878        if let Some(buf) = &mut self.capture {
2879            buf.extend_from_slice(bytes);
2880            return;
2881        }
2882        use std::io::Write as _;
2883        if stderr {
2884            let mut e = std::io::stderr();
2885            let _ = e.write_all(bytes);
2886            let _ = e.flush();
2887        } else {
2888            let mut o = std::io::stdout();
2889            let _ = o.write_all(bytes);
2890            let _ = o.flush();
2891        }
2892    }
2893    pub fn del_name(&mut self, name: &str) {
2894        if self
2895            .cur_env()
2896            .borrow_mut()
2897            .vars
2898            .shift_remove(name)
2899            .is_some()
2900        {
2901            return;
2902        }
2903        self.globals.shift_remove(name);
2904    }
2905
2906    pub fn current_this(&self) -> Option<Value> {
2907        self.frame().this_obj.clone()
2908    }
2909
2910    /// The running activation's [`ThisState`].
2911    pub fn this_state(&self) -> ThisState {
2912        self.frame().this_state
2913    }
2914
2915    /// Mark the next user-function activation as a derived constructor.
2916    pub fn mark_next_call_derived_ctor(&mut self) {
2917        self.derived_ctor_next = true;
2918    }
2919
2920    /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2921    /// nearest derived-constructor activation becomes `Bound`. That is the top
2922    /// frame, or — for `super()` inside an arrow — the constructor below the
2923    /// arrow's own frame. `false` when it was already bound: the second call.
2924    pub fn bind_super_this(&mut self) -> bool {
2925        let Some(f) = self
2926            .frames
2927            .iter_mut()
2928            .rev()
2929            .find(|f| f.this_state != ThisState::Plain)
2930        else {
2931            return true;
2932        };
2933        if f.this_state == ThisState::Bound {
2934            return false;
2935        }
2936        f.this_state = ThisState::Bound;
2937        true
2938    }
2939
2940    /// The object a `super()` call substituted for the instance, if any.
2941    ///
2942    /// `construct_class` allocates the instance up front, so when a base
2943    /// constructor RETURNS an object the substitution happens deep inside the
2944    /// VM, after that allocation. This carries it back out. Each
2945    /// `construct_class` saves and restores the previous value around its own
2946    /// run, so a `new` inside a constructor body cannot steal it.
2947    pub fn take_super_replacement(&mut self) -> Option<Value> {
2948        self.super_replacement.take()
2949    }
2950
2951    pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2952        std::mem::replace(&mut self.super_replacement, v)
2953    }
2954
2955    /// Rebind the running activation's `this`.
2956    ///
2957    /// Only `super()` does this: when the parent constructor RETURNS an object,
2958    /// 15.7.15 makes that object the derived instance, so the rest of the
2959    /// derived constructor has to write to it rather than to the one allocated
2960    /// before the call.
2961    pub fn set_current_this(&mut self, v: Value) {
2962        if let Some(f) = self.frames.last_mut() {
2963            f.this_obj = Some(v.clone());
2964        }
2965        self.super_replacement = Some(v);
2966    }
2967    /// The callbacks to run for `event`, consuming any `once` registration in
2968    /// the same step — so a listener that re-emits the event cannot re-enter a
2969    /// one-shot handler.
2970    pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2971        let Some(list) = self.process_listeners.get_mut(event) else {
2972            return Vec::new();
2973        };
2974        let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2975        list.retain(|l| !l.once);
2976        fired
2977    }
2978
2979    /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2980    ///
2981    /// Node answers differently per entry point and both answers are objects:
2982    /// `node f.js` runs a CommonJS module, so top-level `this` is
2983    /// `module.exports`; `node -e` and `node -` run a Script, so it is
2984    /// `globalThis`. Verified on node v26.7.0 —
2985    /// `console.log(this === globalThis, this === module.exports)` is
2986    /// `false true` from a file and `true false` from `-e` and from stdin. It
2987    /// was `undefined` at every entry point here, so `this.x = 1` at module
2988    /// scope threw instead of populating the exports object.
2989    ///
2990    /// Only the base frame is touched: a plain function call still gets its own
2991    /// (`undefined`) binding rather than inheriting this one.
2992    pub fn set_top_this(&mut self, v: Value) {
2993        if let Some(f) = self.frames.first_mut() {
2994            f.this_obj = Some(v);
2995        }
2996    }
2997    pub fn current_env_capture(&self) -> Env {
2998        self.frame().env.clone()
2999    }
3000    pub fn current_new_target(&self) -> Option<Value> {
3001        self.frame().new_target.clone()
3002    }
3003    fn current_home_class(&self) -> Option<Value> {
3004        self.frame().home_class.clone()
3005    }
3006
3007    /// The `(parent_ctor, this_class_fields)` for a running constructor's
3008    /// `super(...)`, derived from the frame's home class.
3009    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3010        match self.current_home_class() {
3011            Some(cv) => match self.get(&cv) {
3012                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3013                _ => (None, Vec::new()),
3014            },
3015            None => (None, Vec::new()),
3016        }
3017    }
3018
3019    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3020    /// by the caller, outside any host borrow) or a directly-usable value.
3021    pub fn super_resolve(&self, name: &str) -> SuperRef {
3022        // A shorthand method in an OBJECT LITERAL resolves `super` through its
3023        // home object's prototype; only a class method has a home CLASS. With
3024        // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3025        // parent to look in and reported the method missing.
3026        if let Some(home) = self.frame().home_object.clone() {
3027            let target = self.proto_of(&home).unwrap_or(Value::Undef);
3028            if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3029                return SuperRef::Getter(getter);
3030            }
3031            return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3032        }
3033        let parent = match self
3034            .current_home_class()
3035            .and_then(|cv| match self.get(&cv) {
3036                Some(JsObj::Class(c)) => c.parent.clone(),
3037                _ => None,
3038            }) {
3039            Some(p) => p,
3040            None => return SuperRef::Data(Value::Undef),
3041        };
3042        // A STATIC method's home object is the constructor, so `super.x` reads
3043        // off the parent CONSTRUCTOR; an instance method's is the prototype
3044        // object, so it reads off the parent's prototype. Always taking the
3045        // prototype meant `static s() { return super.s(); }` found nothing and
3046        // then tried to call it.
3047        let target = if self.frame().home_static {
3048            parent.clone()
3049        } else {
3050            match self.get(&parent) {
3051                Some(JsObj::Class(pc)) => pc.proto.clone(),
3052                _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3053            }
3054        };
3055        if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3056            return SuperRef::Getter(getter);
3057        }
3058        if let Some(v) = lookup_chain(self, &target, name) {
3059            return SuperRef::Data(v);
3060        }
3061        // A static method lives in the fn-prop side table, not the property map.
3062        SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3063    }
3064
3065    // ── signals / errors ─────────────────────────────────────────────────
3066    pub fn take_error(&mut self) -> Option<String> {
3067        self.error.take()
3068    }
3069    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3070        let s = if msg.is_empty() {
3071            class.to_string()
3072        } else {
3073            format!("{class}: {msg}")
3074        };
3075        self.error = Some(s.clone());
3076        s
3077    }
3078}
3079
3080// ── error constructors ───────────────────────────────────────────────────────
3081
3082pub fn type_error(msg: &str) -> String {
3083    format!("TypeError: {msg}")
3084}
3085pub fn ref_error(name: &str) -> String {
3086    format!("ReferenceError: {name} is not defined")
3087}
3088
3089/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3090/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3091/// happened, and the difference is how a reader tells a misspelled name from a
3092/// `let` used above its declaration.
3093pub fn tdz_error(name: &str) -> String {
3094    format!("ReferenceError: Cannot access '{name}' before initialization")
3095}
3096pub fn range_error(msg: &str) -> String {
3097    format!("RangeError: {msg}")
3098}
3099
3100/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3101/// largest string the engine will materialize.
3102///
3103/// Measured on node v26.7.0 (darwin arm64):
3104/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3105/// `'a'.repeat(536870888)` succeeds with that length, and
3106/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3107pub const MAX_STRING_LENGTH: usize = 536_870_888;
3108
3109/// The error V8 raises for a string operation whose RESULT would exceed
3110/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3111/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3112/// sit building a 1 TiB `String` until it was killed.
3113pub fn invalid_string_length() -> String {
3114    range_error("Invalid string length")
3115}
3116
3117/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3118/// and 10.4.2.4 `ArraySetLength` step 3.
3119///
3120/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3121/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3122/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3123/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3124/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3125/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3126/// materializing four billion elements for `a.length = 2**32`.
3127pub fn to_array_length(v: &Value) -> Result<usize, String> {
3128    // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3129    // `ToNumber(value)`, compared against each other. Both are observable — a
3130    // counting `valueOf` sees two calls in node and saw one here — and the
3131    // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3132    let u32_pass = to_number_value(v)?;
3133    let n = to_number_value(v)?;
3134    let _ = u32_pass;
3135    // `ToUint32`: truncate toward zero, then modulo 2^32.
3136    let u = if n.is_finite() {
3137        (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3138    } else {
3139        0
3140    };
3141    // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3142    if (u as f64) != n {
3143        return Err(range_error("Invalid array length"));
3144    }
3145    Ok(u as usize)
3146}
3147
3148/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3149///
3150/// `builtins::synth_error` parses that head back apart, so the bracketed code
3151/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3152/// handling reads. Writing the head by hand at each throw site is what left a
3153/// dozen of them with `err.code === undefined` while the message matched.
3154///
3155/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3156/// is left bracketed while the stack is captured and therefore shows up in both
3157/// `String(err)` and `err.stack` — measured on v26.7.0:
3158///
3159/// ```text
3160/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3161/// ```
3162pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3163    format!("{class} [{code}]: {msg}")
3164}
3165
3166/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3167pub const CODE_MARK: &str = "\u{1}code:";
3168
3169/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3170/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3171/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3172/// `synth_error` could otherwise recognise.
3173pub const DOM_MARK: &str = "\u{1}dom:";
3174
3175/// A `DOMException` error string: `name` is the WHATWG error name.
3176pub fn dom_error(name: &str, msg: &str) -> String {
3177    format!("{DOM_MARK}{name}\u{1}{msg}")
3178}
3179
3180/// A Node coded error raised from the *native* layer: `.code` is set, but the
3181/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3182///
3183/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3184/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3185/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3186/// brackets its code into the very same two reads. Encoding both through one
3187/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3188///
3189/// The code rides in a marker at the head of the message rather than in the
3190/// error class, because the class text is exactly what must NOT carry it. The
3191/// marker is an internal wire format between a throw site and `synth_error`; it
3192/// never survives into a `.message`.
3193pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3194    format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3195}
3196
3197/// Marks the start of the extra string own properties a
3198/// [`plain_coded_error_with`] error carries after its message.
3199pub const FIELDS_MARK: char = '\u{2}';
3200
3201/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3202/// `code` in the order given — `new URL('x', 'nope')` throws with
3203/// `Object.keys(e)` reading `["code","input","base"]`.
3204///
3205/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3206/// arbitrary user text) may carry any character, the separators included.
3207pub fn plain_coded_error_with(class: &str, code: &str, msg: &str, fields: &[(&str, &str)]) -> String {
3208    let mut s = plain_coded_error(class, code, msg);
3209    s.push(FIELDS_MARK);
3210    for (k, v) in fields {
3211        s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3212    }
3213    s
3214}
3215
3216/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3217/// internal code and field markers of [`plain_coded_error`] /
3218/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3219/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3220pub fn plain_error_text(e: &str) -> String {
3221    let Some(i) = e.find(CODE_MARK) else {
3222        return e.to_string();
3223    };
3224    let (head, rest) = e.split_at(i);
3225    match rest[CODE_MARK.len()..].split_once('\u{1}') {
3226        Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3227        None => e.to_string(),
3228    }
3229}
3230
3231/// Split a [`plain_coded_error_with`] message back into the message and its
3232/// fields. A message with no field mark comes back whole with no fields.
3233pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3234    let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3235        return (msg, Vec::new());
3236    };
3237    let mut fields = Vec::new();
3238    while let Some((k, tail)) = rest.split_once('\u{3}') {
3239        let Some((len, tail)) = tail.split_once('\u{3}') else { break };
3240        let Ok(len) = len.parse::<usize>() else { break };
3241        let Some(v) = tail.get(..len) else { break };
3242        fields.push((k, v));
3243        rest = &tail[len..];
3244    }
3245    (head, fields)
3246}
3247
3248/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3249/// <expected>. Received …` — Node's single most common argument rejection.
3250pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3251    coded_error(
3252        "TypeError",
3253        "ERR_INVALID_ARG_TYPE",
3254        &format!(
3255            "The \"{name}\" {kind} must be of type {expected}. Received {}",
3256            crate::stdlib::received_desc(v)
3257        ),
3258    )
3259}
3260
3261// ── the fusevm run plumbing ──────────────────────────────────────────────────
3262
3263thread_local! {
3264    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3265}
3266
3267/// Enable/disable DAP debug execution (`node --dap`).
3268pub fn set_debug_mode(on: bool) {
3269    DEBUG_MODE.with(|d| d.set(on));
3270}
3271
3272// ── join cycle detection ─────────────────────────────────────────────────────
3273
3274thread_local! {
3275    /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3276    static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3277}
3278
3279/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3280/// it already is.
3281///
3282/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3283/// it) is the one place the language walks an object graph with no depth bound,
3284/// so every engine cuts re-entrance here: a receiver already on the stack
3285/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3286/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3287/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3288/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3289/// uncatchable, where node returns a string.
3290///
3291/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3292/// twice, because the first render pops before the second pushes.
3293///
3294/// A `true` return MUST be paired with [`join_stack_pop`].
3295pub fn join_stack_push(v: &Value) -> bool {
3296    match v {
3297        Value::Obj(i) => JOIN_STACK.with(|s| {
3298            let mut s = s.borrow_mut();
3299            if s.contains(i) {
3300                false
3301            } else {
3302                s.push(*i);
3303                true
3304            }
3305        }),
3306        _ => true,
3307    }
3308}
3309
3310/// Pop the innermost [`join_stack_push`].
3311pub fn join_stack_pop() {
3312    JOIN_STACK.with(|s| {
3313        s.borrow_mut().pop();
3314    });
3315}
3316
3317// ── native stack guard ───────────────────────────────────────────────────────
3318
3319thread_local! {
3320    /// Lowest stack address a nested run may start from, or 0 before the
3321    /// running thread's bounds have been measured. Cached because the pthread
3322    /// query is a syscall-free but non-trivial read and this is on every call.
3323    static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3324}
3325
3326/// Stack left unusable below the floor, as a fraction of the whole stack: the
3327/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3328/// whatever `catch` receives it, all of which needs room *below* the deepest
3329/// call that was allowed.
3330const STACK_RESERVE_DIVISOR: usize = 8;
3331/// Floor of that reserve, for a thread whose stack is small enough that an
3332/// eighth of it would not cover the unwind.
3333const STACK_RESERVE_MIN: usize = 512 * 1024;
3334/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3335/// large relative to a default 8 MiB stack — over-reserving costs recursion
3336/// depth, under-reserving costs the process.
3337const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3338
3339/// The address of a local in the caller's frame — how far down the stack
3340/// execution currently is. `black_box` keeps the probe from being optimized into
3341/// a different frame.
3342fn stack_pointer() -> usize {
3343    let probe = 0u8;
3344    std::hint::black_box(&probe) as *const u8 as usize
3345}
3346
3347/// The running thread's `(lowest address, size)` stack bounds.
3348///
3349/// Asked of pthread rather than assumed, because the three threads that run JS
3350/// have three different stacks: the `node` binary's own (`main.rs` reserves
3351/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3352/// harness thread's. A fixed byte budget would be wrong on two of the three.
3353fn stack_bounds() -> Option<(usize, usize)> {
3354    #[cfg(target_vendor = "apple")]
3355    {
3356        // SAFETY: both calls are pure reads of the calling thread's own
3357        // pthread record; neither allocates nor can fail.
3358        unsafe {
3359            let me = libc::pthread_self();
3360            let top = libc::pthread_get_stackaddr_np(me) as usize;
3361            let size = libc::pthread_get_stacksize_np(me);
3362            if size == 0 || top < size {
3363                return None;
3364            }
3365            Some((top - size, size))
3366        }
3367    }
3368    #[cfg(target_os = "linux")]
3369    {
3370        // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3371        // read, only read on the success path, and destroyed on every path.
3372        unsafe {
3373            let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3374            if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3375                return None;
3376            }
3377            let mut low: *mut libc::c_void = std::ptr::null_mut();
3378            let mut size: libc::size_t = 0;
3379            let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3380            libc::pthread_attr_destroy(&mut attr);
3381            if ok && size != 0 {
3382                return Some((low as usize, size));
3383            }
3384            None
3385        }
3386    }
3387    #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3388    {
3389        None
3390    }
3391}
3392
3393/// The stack address below which a further nested VM run must throw instead of
3394/// recursing.
3395///
3396/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3397/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3398/// and runs the body, whose own calls land back here. Unbounded JS recursion
3399/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3400/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3401/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3402/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3403///
3404/// The floor is derived from the thread's real bounds rather than a frame count
3405/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3406/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3407/// stack and no more), a release build far less, and a native builtin recursing
3408/// through a user callback spends a different amount again.
3409fn stack_floor() -> usize {
3410    let cached = STACK_FLOOR.with(|c| c.get());
3411    if cached != 0 {
3412        return cached;
3413    }
3414    let floor = match stack_bounds() {
3415        Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3416        None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3417    };
3418    STACK_FLOOR.with(|c| c.set(floor));
3419    floor
3420}
3421
3422/// Stack given to each generator/async coroutine.
3423///
3424/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3425/// left a `function*` body barely ten frames of recursion before it walked off
3426/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3427/// a larger one is address space, not resident memory — but it IS per live
3428/// generator, so this stays far below the entry thread's
3429/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3430/// has thousands of these.
3431const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3432
3433/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3434fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3435    stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3436}
3437
3438/// corosensei's own `DefaultStack::default()` size, used only when the
3439/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3440/// on a stack whose bounds are not ours to read.
3441const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3442
3443/// Give a coroutine whose stack bounds are unknown a floor measured from where
3444/// its body starts. Called once, at body entry, on the coroutine's own stack.
3445fn ensure_coroutine_floor() {
3446    if STACK_FLOOR.with(|c| c.get()) != 0 {
3447        return;
3448    }
3449    let budget = CORO_FALLBACK_STACK_SIZE
3450        - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3451    STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3452}
3453
3454/// Install `floor` as the current stack floor, returning the previous one.
3455///
3456/// Used around a coroutine resume, which switches to a stack the thread's
3457/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3458/// the next [`stack_floor`] measure again, which is the right answer for the
3459/// entry thread and a conservative one for a fallback coroutine stack.
3460fn swap_stack_floor(floor: usize) -> usize {
3461    STACK_FLOOR.with(|c| c.replace(floor))
3462}
3463
3464/// Whether the native stack is too close to its floor for one more nested run.
3465pub fn stack_exhausted() -> bool {
3466    stack_pointer() <= stack_floor()
3467}
3468
3469/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3470/// `RangeError` constructor node uses — not a `panic!`.
3471pub fn stack_overflow_error() -> String {
3472    range_error("Maximum call stack size exceeded")
3473}
3474
3475/// Pool key for the body of user function `def_id`.
3476pub fn func_key(def_id: usize) -> u64 {
3477    1 << 40 | def_id as u64
3478}
3479
3480/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3481/// handler, 2 = the finalizer.
3482pub fn try_key(try_id: usize, part: u64) -> u64 {
3483    2 << 40 | (try_id as u64) << 2 | part
3484}
3485
3486thread_local! {
3487    /// VMs that have finished a run, kept for the next one — grouped by the
3488    /// chunk they still hold.
3489    ///
3490    /// Every JS call, every `try` block and every generator step runs its chunk
3491    /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3492    /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3493    /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3494    /// 400k calls, so it built 400k VMs to run 23 ops each.
3495    ///
3496    /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3497    /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3498    /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3499    /// call takes back the VM that already holds that body and copies nothing:
3500    /// `VM::reset` is handed the chunk the VM was already carrying.
3501    ///
3502    /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3503    /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3504    /// a nested (or recursive) call takes the next one, so a key grows to the
3505    /// deepest simultaneous entry into that function and no further.
3506    static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3507        RefCell::new(rustc_hash::FxHashMap::default());
3508}
3509
3510/// An idle VM filed under `key`, if any.
3511fn take_pooled(key: u64) -> Option<VM> {
3512    VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3513}
3514
3515/// File a finished VM under `key` for the next run to take.
3516fn put_pooled(key: u64, vm: VM) {
3517    VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3518}
3519
3520/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3521/// and fitted with the builtins and hooks a fresh VM needs.
3522fn acquire_vm(chunk: Chunk) -> VM {
3523    if let Some(mut vm) = take_pooled(0) {
3524        vm.reset(chunk);
3525        return vm;
3526    }
3527    let mut vm = VM::new(chunk);
3528    crate::builtins::install(&mut vm);
3529    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3530        crate::builtins::numeric_hook(op, a, b)
3531    }));
3532    // Under `--dap` the tracing JIT would compile hot loops and skip the
3533    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3534    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3535    // extension seam mirrors pythonrs should the marker emission ever switch.
3536    // The mode is fixed before the first chunk runs, so a pooled VM can never
3537    // come back wearing the wrong one.
3538    if DEBUG_MODE.with(|d| d.get()) {
3539        vm.set_extension_handler(Box::new(|vm, id, _| {
3540            crate::dap::on_ext(vm, id);
3541        }));
3542    } else {
3543        vm.enable_tracing_jit();
3544    }
3545    vm
3546}
3547
3548/// Register every node-js builtin + the numeric hook on a VM, then run it.
3549///
3550/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3551/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3552/// that runs repeatedly (a function body, a `try` block) goes through
3553/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3554pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3555    // Checked before the `VM` is built: `VM::new` + `install` are themselves
3556    // several KiB of frame, so a check after them could already have overflowed.
3557    if stack_exhausted() {
3558        return Err(stack_overflow_error());
3559    }
3560    finish_run(0, acquire_vm(chunk))
3561}
3562
3563/// Run the chunk filed under `key`, building it with `make` only if no VM is
3564/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3565/// call copies no bytecode at all.
3566pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3567    if stack_exhausted() {
3568        return Err(stack_overflow_error());
3569    }
3570    let vm = match take_pooled(key) {
3571        Some(mut vm) => {
3572            // Hand the VM back the chunk it is already carrying: `reset` takes
3573            // an owned `Chunk`, and this is the one place where the owned chunk
3574            // costs nothing.
3575            let held = std::mem::take(&mut vm.chunk);
3576            vm.reset(held);
3577            vm
3578        }
3579        None => acquire_vm(make()),
3580    };
3581    finish_run(key, vm)
3582}
3583
3584/// Run a prepared VM to completion and file it back under `key`.
3585fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3586    let outcome = vm.run();
3587    let result = match outcome {
3588        _ if with_host(|h| h.error.is_some()) => {
3589            Err(with_host(|h| h.take_error()).expect("just checked"))
3590        }
3591        VMResult::Ok(v) => Ok(v),
3592        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3593        VMResult::Error(e) => Err(e),
3594    };
3595    put_pooled(key, vm);
3596    result
3597}
3598
3599/// Run `chunk` in the GLOBAL scope instead of the caller's.
3600///
3601/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3602/// and can shadow — the *calling function's* locals. That is right for a direct
3603/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3604/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3605/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3606/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3607/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3608/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3609/// is `"undefined"` there and was `"number"` here.
3610///
3611/// A `var` the chunk itself declares lands in the top-level scope and persists,
3612/// so successive `vm.runInThisContext` calls share it.
3613pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3614    // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3615    // to the global object, not to the entry module's wrapper scope. The flag
3616    // that keeps the entry script's own `var`s out of the globals map has to be
3617    // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3618    // `globalThis.g`.
3619    let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3620    let out = run_chunk_in_global_scope_inner(chunk);
3621    with_host(|h| h.module_scope = prev_scope);
3622    out
3623}
3624
3625fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3626    let global_env = with_host(|h| h.global_env.clone());
3627    with_host(|h| {
3628        h.frames.push(Frame {
3629            env: global_env.clone(),
3630            base_env: global_env,
3631            this_obj: None,
3632            new_target: None,
3633            home_class: None,
3634            home_static: false,
3635            home_object: None,
3636            strict: false,
3637            line: 0,
3638            owner: None,
3639            is_module: true,
3640            this_state: ThisState::Plain,
3641        })
3642    });
3643    let r = run_chunk_on(chunk);
3644    with_host(|h| {
3645        h.frames.pop();
3646    });
3647    r
3648}
3649
3650/// Run the top-level program chunk, then drain the event loop (microtasks +
3651/// timers) until quiescent — matching Node, which keeps the process alive while
3652/// pending async work remains.
3653pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3654    with_host(|h| h.module_scope = true);
3655    let r = run_chunk_on(chunk);
3656    with_host(|h| h.signal = None);
3657    if r.is_ok() {
3658        run_event_loop()?;
3659        finish_process_events()?;
3660    }
3661    r
3662}
3663
3664/// The shutdown sequence Node runs once the loop has drained on its own: fire
3665/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3666/// again and `beforeExit` fires again), then fire `exit` exactly once.
3667///
3668/// Neither event fired at all before this existed, so `process.on('exit', …)`
3669/// was a registration with no delivery — a listener whose body printed was
3670/// silently dropped, and one that set `process.exitCode` could not affect the
3671/// status. Measured on node v26.7.0,
3672/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3673///
3674/// An explicit `process.exit()` never reaches here (it leaves the process from
3675/// inside the builtin), and neither does an uncaught exception — matching
3676/// Node, where `beforeExit` is skipped on both paths.
3677fn finish_process_events() -> Result<(), String> {
3678    // Bounded: a `beforeExit` listener that re-arms work every time would spin
3679    // forever, exactly as it does in Node, but a runaway here would hang a
3680    // parity run with no output, so it is capped and then treated as drained.
3681    for _ in 0..1000 {
3682        let code = with_host(|h| h.exit_code).unwrap_or(0);
3683        if !crate::stdlib::process::emit_before_exit(code)? {
3684            break;
3685        }
3686        let more =
3687            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3688        if !more {
3689            break;
3690        }
3691        run_event_loop()?;
3692    }
3693    let code = with_host(|h| h.exit_code).unwrap_or(0);
3694    crate::stdlib::process::emit_exit_event(code)
3695}
3696
3697// ── formatting ───────────────────────────────────────────────────────────────
3698
3699/// Format a JS number exactly as `Number.prototype.toString` does for the common
3700/// range (no exponential-notation threshold handling for very large/small).
3701pub fn fmt_number(f: f64) -> String {
3702    if f.is_nan() {
3703        return "NaN".into();
3704    }
3705    if f.is_infinite() {
3706        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3707    }
3708    if f == 0.0 {
3709        // Covers -0.0 too: (-0).toString() === "0".
3710        return "0".into();
3711    }
3712    if f < 0.0 {
3713        return format!("-{}", js_number_repr(-f));
3714    }
3715    js_number_repr(f)
3716}
3717
3718/// If `k` is an array-index property key, return its numeric value. Per
3719/// ECMAScript, a String property key `P` is an array index iff
3720/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3721/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3722pub fn array_index(k: &str) -> Option<u32> {
3723    if k.is_empty() {
3724        return None;
3725    }
3726    if k == "0" {
3727        return Some(0);
3728    }
3729    // A leading '0' (other than the lone "0" above) is non-canonical.
3730    if k.as_bytes()[0] == b'0' {
3731        return None;
3732    }
3733    if !k.bytes().all(|b| b.is_ascii_digit()) {
3734        return None;
3735    }
3736    match k.parse::<u64>() {
3737        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3738        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3739        _ => None,
3740    }
3741}
3742
3743/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3744/// integer-index keys sort ascending-numeric and precede all string keys; two
3745/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3746/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3747/// they also fall into the stable-insertion-order tail.)
3748pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3749    use std::cmp::Ordering;
3750    match (array_index(a), array_index(b)) {
3751        (Some(x), Some(y)) => x.cmp(&y),
3752        (Some(_), None) => Ordering::Less,
3753        (None, Some(_)) => Ordering::Greater,
3754        (None, None) => Ordering::Equal,
3755    }
3756}
3757
3758/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3759/// place: array-index keys ascending first, then the remaining keys in their
3760/// existing (insertion) order. A no-op unless at least one index key is present,
3761/// so the overwhelmingly common all-string-key object keeps its exact order and
3762/// pays nothing. `IndexMap::sort_by` is a stable sort.
3763pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3764    if props.keys().any(|k| array_index(k).is_some()) {
3765        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3766    }
3767}
3768
3769/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3770///
3771/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3772/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3773/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3774/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3775/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3776/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3777/// `n > 21` or `n ≤ -6`.
3778fn js_number_repr(a: f64) -> String {
3779    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3780    // round-trip digits. Split it into the digit string `s` and exponent `E`.
3781    let sci = format!("{a:e}");
3782    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3783    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3784    let s: String = mant.chars().filter(|c| *c != '.').collect();
3785    let k = s.len() as i32; // number of significant digits
3786    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3787
3788    if k <= n && n <= 21 {
3789        // Integer with trailing zeros: all digits, then n−k zeros.
3790        let mut out = s;
3791        out.push_str(&"0".repeat((n - k) as usize));
3792        out
3793    } else if 0 < n && n <= 21 {
3794        // Decimal point inside the digit run: n digits, '.', the rest.
3795        format!("{}.{}", &s[..n as usize], &s[n as usize..])
3796    } else if -6 < n && n <= 0 {
3797        // Leading "0." then (−n) zeros then all digits.
3798        format!("0.{}{}", "0".repeat((-n) as usize), s)
3799    } else {
3800        // Exponential form. Exponent digit is n−1, always signed.
3801        let exp = n - 1;
3802        let sign = if exp >= 0 { '+' } else { '-' };
3803        let mag = exp.abs();
3804        if k == 1 {
3805            format!("{s}e{sign}{mag}")
3806        } else {
3807            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3808        }
3809    }
3810}
3811
3812impl JsHost {
3813    /// The `typeof` string for `v`.
3814    pub fn type_of(&self, v: &Value) -> &'static str {
3815        match v {
3816            Value::Undef => "undefined",
3817            Value::Bool(_) => "boolean",
3818            Value::Int(_) | Value::Float(_) => "number",
3819            Value::Str(_) => "string",
3820            Value::Obj(_) => match self.get(v) {
3821                Some(JsObj::Str(_)) => "string",
3822                // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3823                // target is callable, so `typeof` classifies by the target —
3824                // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3825                // is bounded: a proxy of a proxy defers again.
3826                Some(JsObj::Proxy { target, .. }) => {
3827                    let mut cur = target;
3828                    for _ in 0..100 {
3829                        match self.get(cur) {
3830                            Some(JsObj::Proxy { target: t, .. }) => cur = t,
3831                            _ => break,
3832                        }
3833                    }
3834                    if is_callable(self, cur) {
3835                        "function"
3836                    } else {
3837                        "object"
3838                    }
3839                }
3840                Some(JsObj::Func(_))
3841                | Some(JsObj::BoundMethod { .. })
3842                | Some(JsObj::BoundFunc { .. })
3843                | Some(JsObj::Class(_)) => "function",
3844                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3845                // `typeof === "function"` — EXCEPT the non-callable namespace
3846                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3847                Some(JsObj::Builtin(n)) => {
3848                    if builtin_is_callable(n) {
3849                        "function"
3850                    } else {
3851                        "object"
3852                    }
3853                }
3854                Some(JsObj::Symbol { .. }) => "symbol",
3855                Some(JsObj::BigInt(_)) => "bigint",
3856                _ => "object", // arrays, objects, null, Map/Set, generators
3857            },
3858            _ => "object",
3859        }
3860    }
3861
3862    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3863    pub fn truthy(&self, v: &Value) -> bool {
3864        match v {
3865            Value::Undef => false,
3866            Value::Bool(b) => *b,
3867            Value::Int(n) => *n != 0,
3868            Value::Float(f) => *f != 0.0 && !f.is_nan(),
3869            Value::Str(s) => !s.is_empty(),
3870            Value::Obj(_) => match self.get(v) {
3871                Some(JsObj::Str(s)) => !s.is_empty(),
3872                Some(JsObj::Null) => false,
3873                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3874                _ => true, // arrays, objects, functions
3875            },
3876            _ => true,
3877        }
3878    }
3879
3880    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3881    pub fn to_number(&self, v: &Value) -> f64 {
3882        match v {
3883            Value::Undef => f64::NAN,
3884            Value::Bool(b) => {
3885                if *b {
3886                    1.0
3887                } else {
3888                    0.0
3889                }
3890            }
3891            Value::Int(n) => *n as f64,
3892            Value::Float(f) => *f,
3893            Value::Str(s) => str_to_number(s),
3894            Value::Obj(_) => match self.get(v) {
3895                Some(JsObj::Str(s)) => str_to_number(s),
3896                Some(JsObj::Null) => 0.0,
3897                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3898                Some(JsObj::Array(items)) => {
3899                    // [] -> 0, [x] -> ToNumber(x), else NaN.
3900                    if items.is_empty() {
3901                        0.0
3902                    } else if items.len() == 1 {
3903                        self.to_number(&items[0])
3904                    } else {
3905                        f64::NAN
3906                    }
3907                }
3908                _ => f64::NAN,
3909            },
3910            _ => f64::NAN,
3911        }
3912    }
3913
3914    /// `String(v)` — the string-coercion form (raw, unquoted).
3915    pub fn str_of(&self, v: &Value) -> String {
3916        match v {
3917            Value::Undef => "undefined".into(),
3918            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3919            Value::Int(n) => n.to_string(),
3920            Value::Float(f) => fmt_number(*f),
3921            Value::Str(s) => (**s).clone(),
3922            Value::Obj(_) => match self.get(v) {
3923                Some(JsObj::Str(s)) => s.clone(),
3924                Some(JsObj::Null) => "null".into(),
3925                Some(JsObj::BigInt(b)) => b.to_string(),
3926                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3927                Some(JsObj::Array(items)) => {
3928                    // Array.prototype.toString: comma-join, null/undefined -> "".
3929                    // Guarded by the JoinStack (see `join_stack_push`) so a
3930                    // self-referential array yields "" instead of recursing until
3931                    // the native stack aborts the process.
3932                    if !join_stack_push(v) {
3933                        return String::new();
3934                    }
3935                    let parts: Vec<String> = items
3936                        .iter()
3937                        .map(|x| match x {
3938                            Value::Undef => String::new(),
3939                            _ if self.is_null(x) => String::new(),
3940                            _ => self.str_of(x),
3941                        })
3942                        .collect();
3943                    join_stack_pop();
3944                    parts.join(",")
3945                }
3946                Some(JsObj::Object(props)) => {
3947                    // A native `Buffer` stringifies to its decoded (utf-8)
3948                    // contents, matching `buf.toString()` — needed for `'' + buf`,
3949                    // template interpolation, and `data += chunk` (the pattern
3950                    // Express/body-parser use to read a request body).
3951                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3952                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3953                            Some(JsObj::Array(items)) => {
3954                                items.iter().map(|x| self.to_number(x) as u8).collect()
3955                            }
3956                            _ => Vec::new(),
3957                        };
3958                        String::from_utf8_lossy(&bytes).into_owned()
3959                    } else if let Some(s) = self.error_to_string(v) {
3960                        s
3961                    } else {
3962                        "[object Object]".into()
3963                    }
3964                }
3965                Some(JsObj::Func(f)) => {
3966                    // A function built from runtime source (`new Function`,
3967                    // `vm.compileFunction`) retains the exact text V8 synthesizes
3968                    // for it, so `Function.prototype.toString` reports what Node
3969                    // reports. Every other function slices its span out of the
3970                    // script it was parsed from; only one whose program kept no
3971                    // text (an AOT image, a `rust { }` desugared file) falls back
3972                    // to the placeholder.
3973                    if let Some(src) = self.fn_prop(v, "@@source") {
3974                        return self.str_of(&src);
3975                    }
3976                    if let Some(text) = self.func_source(f.def_id) {
3977                        return text.to_string();
3978                    }
3979                    let name = self
3980                        .funcs
3981                        .get(f.def_id)
3982                        .map(|d| d.name.clone())
3983                        .unwrap_or_default();
3984                    format!("function {name}() {{ [code] }}")
3985                }
3986                // The native-code form names the FUNCTION, not its key:
3987                // `String(Math.max)` is `function max() { [native code] }`.
3988                Some(JsObj::Builtin(n)) => {
3989                    // The `console` methods are the exception node itself makes:
3990                    // each is a wrapper, so `String(console.log)` is the
3991                    // ANONYMOUS native-code form even though `console.log.name`
3992                    // is `log`. Measured on v26.8.1.
3993                    if n.starts_with("console.") {
3994                        "function () { [native code] }".into()
3995                    } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
3996                        // An accessor half names itself `get size` / `set
3997                        // arguments`, which `builtin_name` cannot build because
3998                        // it returns a borrowed `&str`.
3999                        format!("function {accessor}() {{ [native code] }}")
4000                    } else {
4001                        format!(
4002                            "function {}() {{ [native code] }}",
4003                            crate::builtins::builtin_name(n)
4004                        )
4005                    }
4006                }
4007                // A method read off an instance names itself the same way the
4008                // prototype method it resolves to does: `String([].slice)` is
4009                // `function slice() { [native code] }`.
4010                Some(JsObj::BoundMethod { name, .. }) => {
4011                    format!("function {name}() {{ [native code] }}")
4012                }
4013                Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4014                // `Function.prototype.toString` refuses to expose a proxy's
4015                // target: V8 reports the native-code form for a proxy of ANY
4016                // callable, so `String(new Proxy(function f(){}, {}))` is
4017                // `function () { [native code] }`, not `f`'s source.
4018                Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4019                    "function () { [native code] }".into()
4020                }
4021                Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4022                    Some(text) => text.to_string(),
4023                    None => format!("class {} {{ }}", c.name),
4024                },
4025                Some(JsObj::Symbol { desc, .. }) => {
4026                    // `String(sym)` is allowed (unlike implicit coercion) and yields
4027                    // `Symbol(desc)`.
4028                    match desc {
4029                        Some(d) => format!("Symbol({d})"),
4030                        None => "Symbol()".into(),
4031                    }
4032                }
4033                _ => "[object Object]".into(),
4034            },
4035            _ => "[object Object]".into(),
4036        }
4037    }
4038
4039    /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4040    /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4041    /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4042    /// entry and showing it twice would be wrong.
4043    ///
4044    /// Only a DATA property is seen. A tag supplied by a prototype getter
4045    /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4046    /// which cannot run under the host borrow `inspect` holds — such an object
4047    /// prints without the prefix.
4048    /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4049    /// a primitive wrapper, distinguishing it from the bare primitive.
4050    fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4051        let prim = match self.get(v) {
4052            Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4053            _ => return None,
4054        };
4055        let ctor = match &prim {
4056            Value::Bool(_) => "Boolean",
4057            Value::Int(_) | Value::Float(_) => "Number",
4058            // BigInt and Symbol primitives live on the heap; their boxes are
4059            // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4060            _ => match self.get(&prim) {
4061                Some(JsObj::BigInt(_)) => "BigInt",
4062                Some(JsObj::Symbol { .. }) => "Symbol",
4063                _ => "String",
4064            },
4065        };
4066        let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4067        // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4068        // boxed characters are NOT extras — node hides the index properties of
4069        // a String wrapper, showing only what was added to it.
4070        let width = if ctor == "String" {
4071            self.str_of(&prim).chars().count()
4072        } else {
4073            0
4074        };
4075        let extras: Vec<String> = match self.get(v) {
4076            Some(JsObj::Object(p)) => p
4077                .iter()
4078                .filter(|(k, _)| {
4079                    !k.starts_with("@@")
4080                        && !k.starts_with('#')
4081                        && self.prop_attrs(v, k).enumerable
4082                        && !k.parse::<usize>().is_ok_and(|i| i < width)
4083                })
4084                .map(|(k, val)| {
4085                    format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4086                })
4087                .collect(),
4088            _ => Vec::new(),
4089        };
4090        if extras.is_empty() {
4091            return Some(head);
4092        }
4093        Some(self.render_object(&extras, &format!("{head} "), indent, st))
4094    }
4095
4096    /// The `key: value` parts for own properties a script attached to an exotic
4097    /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4098    /// prints `Map(1) { 'k' => 1, x: 5 }`.
4099    fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4100        self.fn_prop_keys(v)
4101            .into_iter()
4102            .filter(|k| {
4103                !k.starts_with("@@")
4104                    && !k.starts_with('#')
4105                    && !is_symbol_key(k)
4106                    && self.prop_attrs(v, k).enumerable
4107            })
4108            .map(|k| {
4109                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4110                format!(
4111                    "{}: {}",
4112                    fmt_key(&k),
4113                    self.inspect_lvl(&val, indent + 2, st)
4114                )
4115            })
4116            .collect()
4117    }
4118
4119    fn inspect_tag(&self, v: &Value) -> Option<String> {
4120        let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4121        if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4122            return None;
4123        }
4124        let t = lookup_chain(self, v, "@@toStringTag")?;
4125        self.as_str(&t)
4126    }
4127
4128    /// `console.log`-style rendering of a top-level argument: bare strings print
4129    /// raw; everything else uses `inspect`.
4130    pub fn console_format(&self, v: &Value) -> String {
4131        match v {
4132            Value::Str(_) => self.str_of(v),
4133            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4134            _ => self.inspect(v),
4135        }
4136    }
4137
4138    /// `util.inspect`-style rendering (nested; strings quoted).
4139    pub fn inspect(&self, v: &Value) -> String {
4140        self.inspect_lvl(v, 0, &mut InspectCycles::default())
4141    }
4142
4143    /// `util.inspect` at a given indentation level, with the cycle guard applied
4144    /// around the object cases.
4145    ///
4146    /// A value already being rendered further up the chain is a CYCLE, and Node
4147    /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4148    /// object it points back at is prefixed `<ref *N>`. Without this the walk
4149    /// only stopped when the depth limit turned the back-edge into `[Object]`,
4150    /// so `const c={a:1}; c.c=c` printed the misleading
4151    /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4152    /// `<ref *1> { a: 1, c: [Circular *1] }`.
4153    ///
4154    /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4155    /// the target's own children are being rendered — so the prefix can only be
4156    /// decided after `inspect_value` returns.
4157    /// Whether `v` renders as a LEAF — a finished string produced without
4158    /// recursing into any child.
4159    ///
4160    /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4161    /// after the early returns for the shapes that answer immediately: a bare
4162    /// Date is its ISO string, a regex is its literal, an empty container is its
4163    /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4164    /// those record a depth, so a group containing one is not pushed over the
4165    /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4166    /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4167    fn renders_without_expanding(&self, v: &Value) -> bool {
4168        let plain_props = |p: &IndexMap<String, Value>| {
4169            p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4170        };
4171        match self.get(v) {
4172            // A regex never recurses, with or without its hidden `lastIndex`.
4173            Some(JsObj::RegExp(_)) => true,
4174            Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4175            Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4176            Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4177            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4178                Some("Buffer") => inspect_custom(),
4179                // Own properties added to a Date DO get expanded after it.
4180                Some("Date") => plain_props(p),
4181                Some(_) => false,
4182                None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4183            },
4184            _ => false,
4185        }
4186    }
4187
4188    fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4189        if !matches!(v, Value::Obj(_)) {
4190            return self.inspect_value(v, indent, st);
4191        }
4192        if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4193            return format!("[Circular *{}]", st.mark(self, v));
4194        }
4195        st.seen.push(v.clone());
4196        // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4197        // it expands — not a running maximum — so after the children have been
4198        // rendered it holds the depth of the last chain below this group, which
4199        // is what `reduceToSingleString` compares. A value the depth limit
4200        // stubs out as `[Object]` is never expanded and must not count, or an
4201        // object whose deepest level was elided would break where node joins.
4202        // Only a value node actually EXPANDS advances the depth. A string,
4203        // symbol or bigint is a JS primitive that this host happens to store on
4204        // the heap, so it reaches here as `Value::Obj` where an unboxed number
4205        // returns above — and counting it as a level made any group holding one
4206        // look deeper than it was. Under `compact: 1` that is the difference
4207        // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4208        // Map across four lines, because its string KEYS were being charged a
4209        // nesting level.
4210        if indent as i64 <= inspect_indent_limit()
4211            && !is_primitive(self, v)
4212            && !self.renders_without_expanding(v)
4213        {
4214            st.deepest = indent;
4215        }
4216        let body = self.inspect_value(v, indent, st);
4217        st.seen.pop();
4218        match st.id_of(self, v) {
4219            Some(id) => format!("<ref *{id}> {body}"),
4220            None => body,
4221        }
4222    }
4223
4224    /// The rendering itself, once `inspect_lvl` has established that `v` is not
4225    /// a back-edge into an object already on the stack.
4226    fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4227        if let Some(s) = self.inspect_wrapper(v, indent, st) {
4228            return s;
4229        }
4230        match v {
4231            Value::Undef => "undefined".into(),
4232            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4233            Value::Int(n) => n.to_string(),
4234            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4235            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4236            Value::Float(f) => fmt_number(*f),
4237            Value::Str(s) => quote_str(s),
4238            Value::Obj(_) => match self.get(v) {
4239                Some(JsObj::Str(s)) => quote_str(s),
4240                Some(JsObj::Null) => "null".into(),
4241                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4242                Some(JsObj::BigInt(b)) => format!("{b}n"),
4243                // `lastIndex` is a non-enumerable own property of every regex,
4244                // so `showHidden` (and therefore `%o`) appends it:
4245                // `/x/g { [lastIndex]: 0 }`.
4246                Some(JsObj::RegExp(r)) => {
4247                    let body = format!("/{}/{}", r.source, r.flags);
4248                    if inspect_show_hidden() {
4249                        format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4250                    } else {
4251                        body
4252                    }
4253                }
4254                // `util.inspect` on node v26.7.0 renders a proxy as
4255                // `Proxy(<target>)` — the target's own rendering, wrapped. It
4256                // deliberately does NOT run the handler's traps, so this stays a
4257                // pure `&self` read like every other inspect arm.
4258                Some(JsObj::Proxy { target, .. }) => {
4259                    format!("Proxy({})", self.inspect_lvl(target, indent, st))
4260                }
4261                // `arguments` is backed by an Array but is an ordinary-shaped
4262                // exotic to util.inspect: node prints its indices as quoted
4263                // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4264                Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4265                    if indent as i64 > inspect_indent_limit() {
4266                        return "[Arguments]".into();
4267                    }
4268                    let mut inner: Vec<String> = items
4269                        .iter()
4270                        .enumerate()
4271                        .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4272                        .collect();
4273                    for k in self.fn_prop_keys(v) {
4274                        if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4275                            continue;
4276                        }
4277                        let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4278                        inner.push(format!(
4279                            "{}: {}",
4280                            fmt_key(&k),
4281                            self.inspect_lvl(&val, indent + 2, st)
4282                        ));
4283                    }
4284                    if inner.is_empty() {
4285                        return "[Arguments] {}".into();
4286                    }
4287                    self.render_object(&inner, "[Arguments] ", indent, st)
4288                }
4289                Some(JsObj::Array(items)) => {
4290                    // Own enumerable non-index string props (e.g. a `str.match(re)`
4291                    // result's `index`/`input`/`groups`, or a user-assigned
4292                    // `arr.foo`) render after the elements, as `key: value`.
4293                    let prop_keys: Vec<String> = self
4294                        .fn_prop_keys(v)
4295                        .into_iter()
4296                        .filter(|k| {
4297                            !k.starts_with("@@")
4298                                && !k.starts_with('#')
4299                                && self.prop_attrs(v, k).enumerable
4300                        })
4301                        .collect();
4302                    // An own enumerable SYMBOL-keyed property renders after the
4303                    // string keys as `Symbol(desc): value`, as it does on an
4304                    // object receiver.
4305                    // An instance of an Array SUBCLASS leads with its
4306                    // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4307                    // `getPrefix` does for any non-`Array` constructor.
4308                    let sub = match self.proto_of(v) {
4309                        Some(_) => self.ctor_name(v),
4310                        None => String::new(),
4311                    };
4312                    let base = if sub.is_empty() || sub == "Array" {
4313                        String::new()
4314                    } else {
4315                        format!("{sub}({}) ", items.len())
4316                    };
4317                    let sym_entries = self.own_symbol_entries(v);
4318                    // Under `showHidden` even an empty array has something to
4319                    // show — node prints `[ [length]: 0 ]`, not `[]`.
4320                    if items.is_empty()
4321                        && prop_keys.is_empty()
4322                        && sym_entries.is_empty()
4323                        && !inspect_show_hidden()
4324                    {
4325                        return format!("{base}[]");
4326                    }
4327                    // Node's default inspect depth is 2 (root = depth 0); deeper
4328                    // nesting collapses to `[Array]`. indent grows by 2 per level.
4329                    if indent as i64 > inspect_indent_limit() {
4330                        return "[Array]".into();
4331                    }
4332                    // `util.inspect`'s `maxArrayLength` (default 100): only the
4333                    // first 100 elements are formatted, and the rest collapse to
4334                    // a `... N more items` entry. Without the cap a 120-element
4335                    // array printed all 120 — and, because the grid column width
4336                    // is computed from what is SHOWN, every column was also one
4337                    // character wider than node's.
4338                    // A SPARSE array takes node's `formatSpecialArray` path: an
4339                    // elided run renders as `<N empty items>` rather than as the
4340                    // `undefined` it reads back as.
4341                    let (mut inner, has_tail) = if self.has_holes(v) {
4342                        self.inspect_sparse(v, items, indent, st)
4343                    } else {
4344                        let shown = items.len().min(inspect_max_array_length());
4345                        let mut inner: Vec<String> = items[..shown]
4346                            .iter()
4347                            .map(|x| self.inspect_lvl(x, indent + 2, st))
4348                            .collect();
4349                        let remaining = items.len() - shown;
4350                        if remaining > 0 {
4351                            let unit = if remaining == 1 { "item" } else { "items" };
4352                            inner.push(format!("... {remaining} more {unit}"));
4353                        }
4354                        (inner, remaining > 0)
4355                    };
4356                    // `showHidden` exposes the non-enumerable `length`, which an
4357                    // array always has. It sorts BEFORE any own property node
4358                    // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4359                    // rather than an element, it also turns the column grid off —
4360                    // which is why a ten-element array under `showHidden` prints
4361                    // on one line rather than as a grid.
4362                    let show_hidden = inspect_show_hidden();
4363                    if show_hidden {
4364                        inner.push(format!("[length]: {}", items.len()));
4365                    }
4366                    let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4367                    for k in &prop_keys {
4368                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4369                        inner.push(format!(
4370                            "{}: {}",
4371                            fmt_key(k),
4372                            self.inspect_lvl(&val, indent + 2, st)
4373                        ));
4374                    }
4375                    for (k, val) in &sym_entries {
4376                        let label = match self.symbol_of_key(k) {
4377                            Some(s) => self.inspect(&s),
4378                            None => continue,
4379                        };
4380                        inner.push(format!(
4381                            "{label}: {}",
4382                            self.inspect_lvl(val, indent + 2, st)
4383                        ));
4384                    }
4385                    self.render_array(
4386                        &inner,
4387                        items,
4388                        indent,
4389                        ArrayLayout {
4390                            has_props,
4391                            has_tail,
4392                            base: &base,
4393                        },
4394                        st,
4395                    )
4396                }
4397                // `URLSearchParams` renders its pairs, not its slots:
4398                // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4399                // which is why it is a pair list rather than a Map rendering.
4400                Some(JsObj::Object(props))
4401                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4402                        == Some("URLSearchParams") =>
4403                {
4404                    let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4405                        Some(JsObj::Array(items)) => items.clone(),
4406                        _ => Vec::new(),
4407                    };
4408                    if pairs.is_empty() {
4409                        return "URLSearchParams {}".into();
4410                    }
4411                    let inner: Vec<String> = pairs
4412                        .iter()
4413                        .filter_map(|kv| match self.get(kv) {
4414                            Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4415                                "{} => {}",
4416                                self.inspect_lvl(&p[0], indent + 2, st),
4417                                self.inspect_lvl(&p[1], indent + 2, st)
4418                            )),
4419                            _ => None,
4420                        })
4421                        .collect();
4422                    self.render_object(&inner, "URLSearchParams ", indent, st)
4423                }
4424                // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4425                // constructor and length, then the elements laid out exactly as
4426                // an array's. Without this it fell through to the generic object
4427                // arm and printed the `{ length, byteLength, byteOffset,
4428                // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4429                // which is the whole reason anyone logs one.
4430                Some(JsObj::Object(props))
4431                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4432                        == Some("TypedArray") =>
4433                {
4434                    let kind = props
4435                        .get("@@kind")
4436                        .map(|k| self.str_of(k))
4437                        .unwrap_or_else(|| "TypedArray".into());
4438                    // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4439                    // which this shared borrow cannot allocate as values.
4440                    let elems = crate::stdlib::typedarray::elems_display(self, v);
4441                    // The grid layout sizes its columns from the VALUES; a
4442                    // 64-bit view's come back as `undefined` (no allocation is
4443                    // possible here), which only affects column padding.
4444                    let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4445                    let base = format!("{kind}({}) ", elems.len());
4446                    if indent as i64 > inspect_indent_limit() {
4447                        return format!("[{kind}]");
4448                    }
4449                    let shown = elems.len().min(inspect_max_array_length());
4450                    let mut inner: Vec<String> = elems[..shown].to_vec();
4451                    let remaining = elems.len() - shown;
4452                    if remaining > 0 {
4453                        let unit = if remaining == 1 { "item" } else { "items" };
4454                        inner.push(format!("... {remaining} more {unit}"));
4455                    }
4456                    // A view's whole identity — its element width, its window
4457                    // onto the backing store, and the store itself — is
4458                    // non-enumerable, so `showHidden` is the only way to see it.
4459                    // `util.format('%o', view)` goes through here, since `%o`
4460                    // implies `showHidden`.
4461                    let show_hidden = inspect_show_hidden();
4462                    if show_hidden {
4463                        let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4464                        let byte_offset = props
4465                            .get("byteOffset")
4466                            .map(|x| self.to_number(x))
4467                            .unwrap_or(0.0);
4468                        inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4469                        inner.push(format!("[length]: {}", elems.len()));
4470                        inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4471                        inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4472                        // An ArrayBuffer reached AS a view's backing store is
4473                        // rendered by node WITHOUT its contents — just
4474                        // `ArrayBuffer { [byteLength]: N }` — even though the
4475                        // same buffer inspected on its own leads with
4476                        // `[Uint8Contents]`. Recursing through the normal
4477                        // ArrayBuffer branch therefore printed the bytes twice,
4478                        // once as the view's elements and again as the store's.
4479                        let buf_len = props
4480                            .get("@@buffer")
4481                            .and_then(|b| self.get(b))
4482                            .and_then(|o| match o {
4483                                JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4484                                _ => None,
4485                            })
4486                            .and_then(|b| {
4487                                self.get(&b).map(|o| match o {
4488                                    JsObj::Array(items) => items.len(),
4489                                    _ => 0,
4490                                })
4491                            })
4492                            .unwrap_or(0);
4493                        inner.push(format!(
4494                            "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4495                        ));
4496                    }
4497                    self.render_array(
4498                        &inner,
4499                        &vals,
4500                        indent,
4501                        ArrayLayout {
4502                            has_props: show_hidden,
4503                            has_tail: remaining > 0,
4504                            base: &base,
4505                        },
4506                        st,
4507                    )
4508                }
4509                // An `ArrayBuffer` renders its CONTENTS, which is the only way
4510                // to see them — it exposes no indices of its own:
4511                // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4512                Some(JsObj::Object(props))
4513                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4514                        == Some("ArrayBuffer") =>
4515                {
4516                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4517                        Some(JsObj::Array(items)) => {
4518                            items.iter().map(|x| self.to_number(x) as u8).collect()
4519                        }
4520                        _ => Vec::new(),
4521                    };
4522                    let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4523                    let mut parts = vec![
4524                        format!("[Uint8Contents]: <{}>", hex.join(" ")),
4525                        format!("[byteLength]: {}", bytes.len()),
4526                    ];
4527                    if props.contains_key("@@maxByteLength") {
4528                        let max = props
4529                            .get("@@maxByteLength")
4530                            .map(|m| self.to_number(m))
4531                            .unwrap_or(0.0);
4532                        parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4533                    }
4534                    self.render_object(&parts, "ArrayBuffer ", indent, st)
4535                }
4536                // A `Date` renders as its ISO-8601 form. Its time value lives in
4537                // the internal `@@ms` slot, which the generic object branch below
4538                // does not show, so without this arm every Date printed as `{}` —
4539                // including through `console.log(d)`, inside arrays, objects and
4540                // Maps, and in an `assert` diff.
4541                Some(JsObj::Object(props))
4542                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4543                {
4544                    let base = crate::stdlib::date::inspect_with_host(self, v);
4545                    // Own properties added to a Date follow the date itself, the
4546                    // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4547                    let extra = self.side_table_parts(v, indent, st);
4548                    let mut inner: Vec<String> = props
4549                        .iter()
4550                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4551                        .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4552                        .collect();
4553                    inner.extend(extra);
4554                    if inner.is_empty() {
4555                        return base;
4556                    }
4557                    self.render_object(&inner, &format!("{base} "), indent, st)
4558                }
4559                // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4560                // at 50 with a `... N more byte(s)` tail, exactly as
4561                // `util.inspect` does. Without this a `console.log(buf)` (the
4562                // single most common thing anyone does with a Buffer) printed
4563                // the internal `{ length, byteLength, … }` bookkeeping.
4564                Some(JsObj::Object(props))
4565                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4566                        == Some("Buffer") =>
4567                {
4568                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4569                        Some(JsObj::Array(items)) => {
4570                            items.iter().map(|x| self.to_number(x) as u8).collect()
4571                        }
4572                        _ => Vec::new(),
4573                    };
4574                    // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4575                    // the shape of the object. Under `customInspect: false` node
4576                    // does not call that hook and falls back to the generic
4577                    // byte-view rendering — which is what an `assert` diff shows,
4578                    // since assert inspects with the hook disabled so that a
4579                    // failure names the differing BYTE rather than two opaque hex
4580                    // blobs. The constructor is `Buffer` while the brand is still
4581                    // `Uint8Array`, so node prints both.
4582                    if !inspect_custom() {
4583                        let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4584                        if indent as i64 > inspect_indent_limit() {
4585                            return "[Buffer [Uint8Array]]".into();
4586                        }
4587                        let shown = bytes.len().min(inspect_max_array_length());
4588                        let mut inner: Vec<String> =
4589                            bytes[..shown].iter().map(|b| b.to_string()).collect();
4590                        let vals: Vec<Value> = bytes[..shown]
4591                            .iter()
4592                            .map(|b| Value::Float(*b as f64))
4593                            .collect();
4594                        let remaining = bytes.len() - shown;
4595                        if remaining > 0 {
4596                            let unit = if remaining == 1 { "item" } else { "items" };
4597                            inner.push(format!("... {remaining} more {unit}"));
4598                        }
4599                        return self.render_array(
4600                            &inner,
4601                            &vals,
4602                            indent,
4603                            ArrayLayout {
4604                                has_props: false,
4605                                has_tail: remaining > 0,
4606                                base: &base,
4607                            },
4608                            st,
4609                        );
4610                    }
4611                    const MAX: usize = 50;
4612                    let shown: Vec<String> =
4613                        bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4614                    let mut out = format!("<Buffer {}", shown.join(" "));
4615                    if bytes.len() > MAX {
4616                        let more = bytes.len() - MAX;
4617                        let unit = if more == 1 { "byte" } else { "bytes" };
4618                        out.push_str(&format!(" ... {more} more {unit}"));
4619                    }
4620                    out.push('>');
4621                    out
4622                }
4623                // An Error inspects as its `.stack` — never as an object literal
4624                // exposing the internal `message`/`stack` slots. Any own property
4625                // a script added beyond those follows in braces, as V8 renders
4626                // it: `Error: x\n    at … { code: 'C' }`.
4627                Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4628                    let mut stack = lookup_chain(self, v, "stack")
4629                        .map(|s| self.str_of(&s))
4630                        .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4631                    // A `DOMException` prints its CLASS and then its name —
4632                    // `DOMException [AbortError]: m` — where a plain error
4633                    // prints only its stack head.
4634                    if let Some(JsObj::Object(p)) = self.get(v) {
4635                        if let Some(n) = p.get("@@domName") {
4636                            let name = self.str_of(n);
4637                            stack = format!(
4638                                "DOMException [{name}]{}",
4639                                stack.strip_prefix(&name).unwrap_or(&stack)
4640                            );
4641                        }
4642                    }
4643                    let extra: Vec<String> = self
4644                        .own_enum_key_names(v)
4645                        .into_iter()
4646                        .filter(|k| k != "name")
4647                        .map(|k| {
4648                            let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4649                                Some(JsObj::Object(p)) => {
4650                                    p.get(&k).cloned().unwrap_or(Value::Undef)
4651                                }
4652                                _ => Value::Undef,
4653                            });
4654                            format!(
4655                                "{}: {}",
4656                                fmt_key(&k),
4657                                self.inspect_lvl(&val, indent + 2, st)
4658                            )
4659                        })
4660                        .collect();
4661                    if extra.is_empty() {
4662                        stack
4663                    } else {
4664                        format!("{stack} {{ {} }}", extra.join(", "))
4665                    }
4666                }
4667                Some(JsObj::Object(props)) => {
4668                    // Instances print with their constructor name as a prefix
4669                    // (`C { x: 1 }`); plain objects have none; a null-prototype
4670                    // object (e.g. an `Object.groupBy` result) is tagged
4671                    // `[Object: null prototype]`.
4672                    let ctor = match self.ctor_name(v) {
4673                        n if n.is_empty() => "Object".to_string(),
4674                        n => n,
4675                    };
4676                    let plain_prefix = if ctor == "Object" {
4677                        String::new()
4678                    } else {
4679                        format!("{ctor} ")
4680                    };
4681                    let prefix = if self.inspects_null_proto(v) {
4682                        "[Object: null prototype] ".to_string()
4683                    } else {
4684                        // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4685                        match self.inspect_tag(v) {
4686                            Some(t) if t != ctor => format!("{ctor} [{t}] "),
4687                            _ => plain_prefix.clone(),
4688                        }
4689                    };
4690                    // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4691                    // private class fields; a real symbol-keyed own property is a
4692                    // visible one and renders as `Symbol(desc): value`.
4693                    // An own ACCESSOR has no value to print: node shows the
4694                    // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4695                    // place. It is found through the `@@ord:` marker the
4696                    // property map holds for it, which is also what puts it in
4697                    // declaration order among the data properties. Without this
4698                    // an accessor rendered as nothing at all — `{ get z(){} }`
4699                    // printed `{}`.
4700                    let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4701                        .iter()
4702                        .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4703                            Some(real) => {
4704                                let attrs = self.prop_attrs(v, real);
4705                                let label = match self.own_accessor(v, real)? {
4706                                    (Some(_), Some(_)) => "[Getter/Setter]",
4707                                    (Some(_), None) => "[Getter]",
4708                                    (None, Some(_)) => "[Setter]",
4709                                    (None, None) => return None,
4710                                };
4711                                attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4712                            }
4713                            // Only an ENUMERABLE own property is shown, as node
4714                            // does: a native instance keeps bookkeeping (a
4715                            // `URLSearchParams`'s `size`) as a hidden own slot,
4716                            // and printing it would report a spec getter as data.
4717                            None if !k.starts_with("@@")
4718                                && !k.starts_with('#')
4719                                && self.prop_attrs(v, k).enumerable =>
4720                            {
4721                                Some((fmt_key(k), Ok(val)))
4722                            }
4723                            None => None,
4724                        })
4725                        .collect();
4726                    shown.extend(props.iter().filter_map(|(k, val)| {
4727                        let sym = self.symbol_of_key(k)?;
4728                        self.prop_attrs(v, k)
4729                            .enumerable
4730                            .then(|| (self.inspect(&sym), Ok(val)))
4731                    }));
4732                    if shown.is_empty() {
4733                        return format!("{prefix}{{}}");
4734                    }
4735                    // Depth limit (Node default 2): deeper objects collapse to
4736                    // `[Object]` (or `[ClassName]` for a named instance).
4737                    if indent as i64 > inspect_indent_limit() {
4738                        return if self.inspects_null_proto(v) {
4739                            // Already bracketed (`[Object: null prototype]`).
4740                            prefix.trim_end().to_string()
4741                        } else if plain_prefix.is_empty() {
4742                            "[Object]".into()
4743                        } else {
4744                            format!("[{}]", plain_prefix.trim_end())
4745                        };
4746                    }
4747                    let inner: Vec<String> = shown
4748                        .iter()
4749                        .map(|(k, val)| match val {
4750                            Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4751                            Err(label) => format!("{k}: {label}"),
4752                        })
4753                        .collect();
4754                    self.render_object(&inner, &prefix, indent, st)
4755                }
4756                Some(JsObj::Symbol { desc, .. }) => match desc {
4757                    Some(d) => format!("Symbol({d})"),
4758                    None => "Symbol()".into(),
4759                },
4760                Some(JsObj::Class(c)) => {
4761                    let base = if c.parent.is_some() {
4762                        let pname = c
4763                            .parent
4764                            .as_ref()
4765                            .map(|p| self.callable_name(p))
4766                            .unwrap_or_default();
4767                        format!("[class {} extends {}]", c.name, pname)
4768                    } else {
4769                        format!("[class {}]", c.name)
4770                    };
4771                    self.with_callable_props(v, base, indent, st)
4772                }
4773                // A Map/Set renders its members at the NEXT nesting level, and
4774                // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4775                // array collapses to `[Array]`. Both used to recurse through
4776                // `inspect`, which restarts at indent 0, so the depth gate never
4777                // fired: nesting printed one level too deep at every depth
4778                // (measured on node v26.7.0, four nested Maps print
4779                // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4780                // and a SELF-referential Map or Set recursed forever and aborted
4781                // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4782                // died with `fatal runtime error: stack overflow`, which no
4783                // `try`/`catch` can see. An empty one still prints in full at any
4784                // depth, as `[]`/`{}` do.
4785                // A WEAK collection never shows its contents: node prints
4786                // `WeakMap { <items unknown> }` whether it holds anything or
4787                // not, because the entries are not enumerable by design.
4788                Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4789                Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4790                Some(JsObj::Map { entries, .. }) => {
4791                    let extra = self.side_table_parts(v, indent, st);
4792                    if entries.is_empty() && extra.is_empty() {
4793                        return "Map(0) {}".into();
4794                    }
4795                    if indent as i64 > inspect_indent_limit() {
4796                        return "[Map]".into();
4797                    }
4798                    let mut inner: Vec<String> = entries
4799                        .values()
4800                        .map(|(k, val)| {
4801                            // Sequenced, not nested in one `format!`: both arms
4802                            // need the same `&mut` cycle state.
4803                            let ks = self.inspect_lvl(k, indent + 2, st);
4804                            let vs = self.inspect_lvl(val, indent + 2, st);
4805                            format!("{ks} => {vs}")
4806                        })
4807                        .collect();
4808                    inner.extend(extra);
4809                    // Laid out by the SAME routine as a plain object, not joined
4810                    // onto one line unconditionally. `Map`/`Set` were the only
4811                    // containers that never consulted `breakLength` or `compact`,
4812                    // so every collection wide enough to wrap printed as one long
4813                    // line: node breaks a seven-member Set of ten-character
4814                    // strings across seven lines, and `util.inspect(m, {compact:
4815                    // false})` — which assert's own diff renderer depends on —
4816                    // could not break a Map at all. Node builds these through
4817                    // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4818                    // is this `prefix` (the trailing space is the brace gap).
4819                    let prefix = format!("Map({}) ", entries.len());
4820                    self.render_object(&inner, &prefix, indent, st)
4821                }
4822                Some(JsObj::Set { entries, .. }) => {
4823                    let extra = self.side_table_parts(v, indent, st);
4824                    if entries.is_empty() && extra.is_empty() {
4825                        return "Set(0) {}".into();
4826                    }
4827                    if indent as i64 > inspect_indent_limit() {
4828                        return "[Set]".into();
4829                    }
4830                    let mut inner: Vec<String> = entries
4831                        .values()
4832                        .map(|v| self.inspect_lvl(v, indent + 2, st))
4833                        .collect();
4834                    inner.extend(extra);
4835                    // Same layout routine as a Map (see above). Note node does
4836                    // NOT column-group a wide Set the way it grids an array:
4837                    // `groupArrayElements` is reached only from the list
4838                    // formatter, so a 30-member Set is thirty lines.
4839                    let prefix = format!("Set({}) ", entries.len());
4840                    self.render_object(&inner, &prefix, indent, st)
4841                }
4842                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4843                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4844                    Some(c) => match c.state {
4845                        PromiseState::Pending => "Promise { <pending> }".into(),
4846                        PromiseState::Fulfilled => {
4847                            format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4848                        }
4849                        PromiseState::Rejected => {
4850                            format!(
4851                                "Promise {{ <rejected> {} }}",
4852                                self.inspect_lvl(&c.value, 0, st)
4853                            )
4854                        }
4855                    },
4856                    None => "Promise { <pending> }".into(),
4857                },
4858                Some(JsObj::Func(f)) => {
4859                    // `callable_name`, not the FuncDef name: an anonymous
4860                    // function expression gets its name by inference from the
4861                    // binding it initialises (`const f = function(){}`), and
4862                    // that lands as an own `name` property.
4863                    let name = self.callable_name(v);
4864                    // util.inspect labels a function by its kind, the same
4865                    // string V8 gives it as `Symbol.toStringTag`:
4866                    // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4867                    let kind = match self.funcs.get(f.def_id) {
4868                        Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4869                        Some(d) if d.is_generator => "GeneratorFunction",
4870                        Some(d) if d.is_async => "AsyncFunction",
4871                        _ => "Function",
4872                    };
4873                    let base = if name.is_empty() {
4874                        format!("[{kind} (anonymous)]")
4875                    } else {
4876                        format!("[{kind}: {name}]")
4877                    };
4878                    self.with_callable_props(v, base, indent, st)
4879                }
4880                Some(JsObj::Builtin(n)) => {
4881                    // A namespace object is not a function and must not be
4882                    // printed as one. The three ECMAScript namespaces carry a
4883                    // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4884                    // their members are all non-enumerable, so the braces really
4885                    // are empty. A `require()`d module namespace has no tag and
4886                    // node prints its members, which cannot be rendered here —
4887                    // formatting a member means allocating its value, and this
4888                    // runs under the host borrow.
4889                    if !builtin_is_callable(n) {
4890                        match crate::builtins::well_known_tag(self, v) {
4891                            Some(tag) => format!("Object [{tag}] {{}}"),
4892                            // `Set.prototype` inspects under the CONSTRUCTOR's
4893                            // name, not the key: node prints `Object [Set] {}`.
4894                            None => {
4895                                format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4896                            }
4897                        }
4898                    } else {
4899                        format!("[Function: {}]", crate::builtins::builtin_name(n))
4900                    }
4901                }
4902                // A bound method is not anonymous: it is the prototype method it
4903                // resolves to, so `console.log(new Uint8Array(1).set)` reports
4904                // `[Function: set]`.
4905                Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4906                Some(JsObj::BoundFunc { target, .. }) => {
4907                    let n = self.callable_name(target);
4908                    if n.is_empty() {
4909                        "[Function: bound ]".into()
4910                    } else {
4911                        format!("[Function: bound {n}]")
4912                    }
4913                }
4914                _ => "undefined".into(),
4915            },
4916            _ => "undefined".into(),
4917        }
4918    }
4919
4920    /// Append a callable's own enumerable properties to its `[Function: f]` /
4921    /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4922    /// A callable with none renders as the bare base.
4923    fn with_callable_props(
4924        &self,
4925        v: &Value,
4926        base: String,
4927        indent: usize,
4928        st: &mut InspectCycles,
4929    ) -> String {
4930        let mut inner: Vec<String> = self
4931            .own_enum_key_names(v)
4932            .into_iter()
4933            .map(|k| {
4934                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4935                format!(
4936                    "{}: {}",
4937                    fmt_key(&k),
4938                    self.inspect_lvl(&val, indent + 2, st)
4939                )
4940            })
4941            .collect();
4942        for (k, val) in self.own_symbol_entries(v) {
4943            if let Some(sym) = self.symbol_of_key(&k) {
4944                inner.push(format!(
4945                    "{}: {}",
4946                    self.inspect(&sym),
4947                    self.inspect_lvl(&val, indent + 2, st)
4948                ));
4949            }
4950        }
4951        if inner.is_empty() {
4952            return base;
4953        }
4954        self.render_object(&inner, &format!("{base} "), indent, st)
4955    }
4956
4957    /// Render a non-empty array's already-formatted element strings, applying
4958    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4959    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4960    /// `values` is the raw element list (drives numeric right-alignment); `indent`
4961    /// is the array's own indentation level.
4962    fn render_array(
4963        &self,
4964        output: &[String],
4965        values: &[Value],
4966        indent: usize,
4967        opts: ArrayLayout<'_>,
4968        st: &InspectCycles,
4969    ) -> String {
4970        let ArrayLayout {
4971            has_props,
4972            has_tail,
4973            base,
4974        } = opts;
4975        // Group array elements together if the array has more than six entries.
4976        // Arrays carrying extra own props (`index`/`input`/… on a match result)
4977        // are never grid-grouped — Node lays those out plainly.
4978        // `compact: false` (held as 0) also turns the GRID off, not just the
4979        // single-line join. Node reaches `groupArrayElements` only under
4980        // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
4981        // element per line however many there are; without this gate a 30-element
4982        // array still came back column-aligned in three rows, which is the form
4983        // assert's diff renderer splits on — every array diff would have been
4984        // computed over grid rows instead of elements.
4985        let entries = output.len();
4986        let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
4987            group_array_elements(self, output, values, indent, has_tail)
4988        } else {
4989            (output.to_vec(), false)
4990        };
4991        // A typed array prints its constructor and length ahead of the brackets
4992        // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
4993        // break-length seed, so a long tag wraps the list one entry sooner.
4994        if output.is_empty() {
4995            return format!("{base}[]");
4996        }
4997        // If no grouping happened, try to line everything up on a single line.
4998        if !grouped {
4999            // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5000            let start = output.len() + indent + 1 + base.chars().count() + 10;
5001            if self.may_compact(indent, st) && is_below_break_length(output, start) {
5002                return format!("{base}[ {} ]", output.join(", "));
5003            }
5004        }
5005        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5006        let pad = " ".repeat(indent);
5007        let sep = format!(",\n{pad}  ");
5008        format!("{base}[\n{pad}  {}\n{pad}]", lines.join(&sep))
5009    }
5010
5011    /// Render a non-empty object's already-formatted `key: value` strings with
5012    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5013    /// else one property per line indented by `indent + 2`. `prefix` is the
5014    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5015    /// Mirrors `render_array`'s break decision, including the `compact` depth
5016    /// gate.
5017    /// Whether a group at `indent` may be joined onto one line.
5018    ///
5019    /// Node's `reduceToSingleString`: only while the subtree below this group is
5020    /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5021    /// nothing qualifies and every group breaks.
5022    fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5023        let compact = inspect_compact();
5024        if compact < 1 {
5025            return false;
5026        }
5027        // Levels, not columns: the indent advances by two per level.
5028        let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5029        (depth_below as i64) < compact
5030    }
5031
5032    fn render_object(
5033        &self,
5034        output: &[String],
5035        prefix: &str,
5036        indent: usize,
5037        st: &InspectCycles,
5038    ) -> String {
5039        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5040        // For a tagged object Node folds the tag into `braces[0]` (e.g.
5041        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5042        // prefix (which carries the trailing space) plus the `{`.
5043        // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5044        // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5045        // the object shape), which is observably different from sorting keys
5046        // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5047        // `assert`'s diff renderer depends on this: without it two objects
5048        // carrying the same properties in a different insertion order diffed as
5049        // a wholesale rewrite of every line instead of as equal.
5050        let sorted_output;
5051        let output = if inspect_sorted() {
5052            let mut v = output.to_vec();
5053            v.sort();
5054            sorted_output = v;
5055            &sorted_output[..]
5056        } else {
5057            output
5058        };
5059        let braces0 = prefix.chars().count() + 1;
5060        let start = output.len() + indent + braces0 + 10;
5061        if self.may_compact(indent, st) && is_below_break_length(output, start) {
5062            return format!("{prefix}{{ {} }}", output.join(", "));
5063        }
5064        let pad = " ".repeat(indent);
5065        let sep = format!(",\n{pad}  ");
5066        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
5067    }
5068
5069    /// The `.name` of any callable (function/class/builtin/bound).
5070    pub fn callable_name(&self, v: &Value) -> String {
5071        // A user-set `.name` own property wins.
5072        if let Some(n) = self.fn_prop(v, "name") {
5073            return self.str_of(&n);
5074        }
5075        match self.get(v) {
5076            Some(JsObj::Func(f)) => self
5077                .funcs
5078                .get(f.def_id)
5079                .map(|d| d.name.clone())
5080                .unwrap_or_default(),
5081            Some(JsObj::Class(c)) => c.name.clone(),
5082            // Not the whole key: a builtin's `.name` is its last segment, and a
5083            // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5084            // `.` at all, so this reported the internal spelling verbatim and
5085            // `console.log(Uint8Array.prototype.set)` printed
5086            // `[Function: @proto:TypedArray:set]`.
5087            Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5088            Some(JsObj::BoundFunc { target, .. }) => {
5089                format!("bound {}", self.callable_name(target))
5090            }
5091            Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5092            _ => String::new(),
5093        }
5094    }
5095
5096    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5097
5098    /// Strict equality (`===`): same type and same value, no coercion.
5099    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5100        match (a, b) {
5101            (Value::Undef, Value::Undef) => true,
5102            (Value::Bool(x), Value::Bool(y)) => x == y,
5103            (Value::Str(x), Value::Str(y)) => x == y,
5104            _ => {
5105                // Numbers (NaN !== NaN, +0 === -0).
5106                let an = matches!(a, Value::Int(_) | Value::Float(_));
5107                let bn = matches!(b, Value::Int(_) | Value::Float(_));
5108                if an && bn {
5109                    let x = self.to_number(a);
5110                    let y = self.to_number(b);
5111                    return x == y;
5112                }
5113                // BigInt === BigInt compares by value (each literal is a distinct
5114                // heap cell, so reference identity would be wrong). BigInt is never
5115                // `===` a Number (different types).
5116                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5117                    return x == y;
5118                }
5119                // Heap values.
5120                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5121                    return sa == sb;
5122                }
5123                let na = self.is_null(a);
5124                let nb = self.is_null(b);
5125                if na || nb {
5126                    return na && nb;
5127                }
5128                // A builtin namespace/constructor/prototype is a SINGLETON in JS
5129                // (`Math === Math`, `Array.prototype === Array.prototype`), but
5130                // every bare reference here allocates a fresh handle, so compare
5131                // those by name rather than by heap index.
5132                if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5133                    (self.get(a), self.get(b))
5134                {
5135                    return builtin_identity(x) == builtin_identity(y);
5136                }
5137                // Reference identity for arrays/objects/functions.
5138                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5139            }
5140        }
5141    }
5142
5143    /// Whether `v` is `null` or `undefined`.
5144    pub fn is_nullish(&self, v: &Value) -> bool {
5145        matches!(v, Value::Undef) || self.is_null(v)
5146    }
5147
5148    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5149    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5150    /// `"null"`, or `"object"` (array / plain object / function).
5151    fn js_type(&self, v: &Value) -> &'static str {
5152        match v {
5153            Value::Undef => "undefined",
5154            Value::Bool(_) => "boolean",
5155            Value::Int(_) | Value::Float(_) => "number",
5156            Value::Str(_) => "string",
5157            Value::Obj(_) => match self.get(v) {
5158                Some(JsObj::Str(_)) => "string",
5159                Some(JsObj::Null) => "null",
5160                Some(JsObj::BigInt(_)) => "bigint",
5161                _ => "object",
5162            },
5163            _ => "object",
5164        }
5165    }
5166
5167    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5168    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5169    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5170    /// `[0] == ""` is `false` — never a number coercion of the object.
5171    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5172        // Same type: identical to `===` (number==number, string==string, etc.).
5173        if self.strict_eq(a, b) {
5174            return true;
5175        }
5176        let ta = self.js_type(a);
5177        let tb = self.js_type(b);
5178        // null and undefined are loosely equal only to each other.
5179        if self.is_nullish(a) || self.is_nullish(b) {
5180            return self.is_nullish(a) && self.is_nullish(b);
5181        }
5182        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5183        // values (both-BigInt was already settled by the `strict_eq` above).
5184        if ta == "bigint" || tb == "bigint" {
5185            return self.bigint_loose_eq(a, b);
5186        }
5187        if ta == tb {
5188            // Same type but not strict-equal (and not nullish) ⇒ not equal.
5189            return false;
5190        }
5191        // number ⇄ string: compare as numbers.
5192        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5193            return self.to_number(a) == self.to_number(b);
5194        }
5195        // boolean side coerces to number, then recompares.
5196        if ta == "boolean" {
5197            return self.loose_eq(&Value::Float(self.to_number(a)), b);
5198        }
5199        if tb == "boolean" {
5200            return self.loose_eq(a, &Value::Float(self.to_number(b)));
5201        }
5202        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5203        // then recompare as string==string or number==string.
5204        if ta == "object" && (tb == "number" || tb == "string") {
5205            let pa = self.str_of(a);
5206            return if tb == "string" {
5207                pa == self.str_of(b)
5208            } else {
5209                str_to_number(&pa) == self.to_number(b)
5210            };
5211        }
5212        if tb == "object" && (ta == "number" || ta == "string") {
5213            let pb = self.str_of(b);
5214            return if ta == "string" {
5215                self.str_of(a) == pb
5216            } else {
5217                self.to_number(a) == str_to_number(&pb)
5218            };
5219        }
5220        false
5221    }
5222
5223    /// The numeric-hook arithmetic/relational fallback for non-native operands
5224    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5225    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5226        use NumOp::*;
5227        match op {
5228            Add => {
5229                // `+`: if either operand is a string, concatenate string forms;
5230                // otherwise numeric addition.
5231                let a_str = self.prefers_string(a);
5232                let b_str = self.prefers_string(b);
5233                if a_str || b_str {
5234                    // String concatenation wins even with a bigint operand
5235                    // (`1n + "x"` → `"1x"`).
5236                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
5237                    Ok(self.new_str(s))
5238                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5239                    self.bigint_arith(op, a, b)
5240                } else {
5241                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5242                }
5243            }
5244            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5245                self.bigint_arith(op, a, b)
5246            }
5247            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5248            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5249            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5250            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5251            Pow => Ok(Value::Float(crate::builtins::js_pow(
5252                self.to_number(a),
5253                self.to_number(b),
5254            ))),
5255            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5256            Neg => Ok(Value::Float(-self.to_number(a))),
5257            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5258            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5259            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5260        }
5261    }
5262
5263    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5264    /// which drives `+` toward concatenation. Primitive strings qualify, and so
5265    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5266    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5267    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5268    fn prefers_string(&self, v: &Value) -> bool {
5269        match v {
5270            Value::Str(_) => true,
5271            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5272            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5273            // string primitive either.
5274            Value::Obj(_) => !matches!(
5275                self.get(v),
5276                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5277            ),
5278            _ => false,
5279        }
5280    }
5281
5282    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5283    /// lexicographic, otherwise numeric (NaN yields false).
5284    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5285        use std::cmp::Ordering;
5286        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5287            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5288            x.cmp(&y)
5289        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5290            // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5291            // order once an astral character meets a BMP one — see `utf16`.
5292            crate::utf16::cmp_units(&x, &y)
5293        } else {
5294            let x = self.to_number(a);
5295            let y = self.to_number(b);
5296            match x.partial_cmp(&y) {
5297                Some(o) => o,
5298                None => return false, // NaN operand
5299            }
5300        };
5301        match op {
5302            NumOp::Lt => ord == Ordering::Less,
5303            NumOp::Le => ord != Ordering::Greater,
5304            NumOp::Gt => ord == Ordering::Greater,
5305            NumOp::Ge => ord != Ordering::Less,
5306            _ => false,
5307        }
5308    }
5309
5310    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5311    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5312    /// BigInt with a Number throws, matching Node).
5313    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5314        if self.is_bigint_val(a) || self.is_bigint_val(b) {
5315            return self.bigint_bitwise(tag, a, b);
5316        }
5317        let x = to_int32(self.to_number(a));
5318        let y = to_int32(self.to_number(b));
5319        let r: i64 = match tag {
5320            binop::BITAND => (x & y) as i64,
5321            binop::BITOR => (x | y) as i64,
5322            binop::BITXOR => (x ^ y) as i64,
5323            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5324            binop::SHR => (x >> ((y as u32) & 31)) as i64,
5325            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5326            _ => 0,
5327        };
5328        Ok(Value::Float(r as f64))
5329    }
5330
5331    // ── BigInt operations ────────────────────────────────────────────────────
5332    /// Whether `v` is a heap `BigInt`.
5333    pub fn is_bigint_val(&self, v: &Value) -> bool {
5334        matches!(self.get(v), Some(JsObj::BigInt(_)))
5335    }
5336    /// The `BigInt` value of `v` (a heap bigint), else `None`.
5337    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5338        match self.get(v) {
5339            Some(JsObj::BigInt(b)) => Some(b.clone()),
5340            _ => None,
5341        }
5342    }
5343    /// Allocate a heap `BigInt`.
5344    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5345        self.alloc(JsObj::BigInt(b))
5346    }
5347
5348    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5349    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5350    /// `TypeError` (a string operand is handled as concatenation before we get
5351    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5352    /// exponent.
5353    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5354        use num_traits::{Signed, Zero};
5355        use NumOp::*;
5356        if op == Neg {
5357            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5358            return Ok(self.new_bigint(-x));
5359        }
5360        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5361            (Some(x), Some(y)) => (x, y),
5362            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5363            _ => {
5364                return Err(type_error(
5365                    "Cannot mix BigInt and other types, use explicit conversions",
5366                ))
5367            }
5368        };
5369        let r = match op {
5370            Add => x + y,
5371            Sub => x - y,
5372            Mul => x * y,
5373            Div => {
5374                if y.is_zero() {
5375                    return Err("RangeError: Division by zero".into());
5376                }
5377                x / y // truncates toward zero (matches JS BigInt division)
5378            }
5379            Mod => {
5380                if y.is_zero() {
5381                    return Err("RangeError: Division by zero".into());
5382                }
5383                x % y // sign follows the dividend (truncated), like JS
5384            }
5385            Pow => {
5386                if y.is_negative() {
5387                    return Err("RangeError: Exponent must be positive".into());
5388                }
5389                let exp = num_traits::ToPrimitive::to_u32(&y)
5390                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5391                num_traits::Pow::pow(x, exp)
5392            }
5393            _ => return Err(type_error("unsupported BigInt operation")),
5394        };
5395        Ok(self.new_bigint(r))
5396    }
5397
5398    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5399    /// be BigInt (mixing throws).
5400    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5401        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5402            (Some(x), Some(y)) => (x, y),
5403            _ => {
5404                return Err(type_error(
5405                    "Cannot mix BigInt and other types, use explicit conversions",
5406                ))
5407            }
5408        };
5409        let r = match tag {
5410            binop::BITAND => x & y,
5411            binop::BITOR => x | y,
5412            binop::BITXOR => x ^ y,
5413            binop::SHL => {
5414                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5415                if n >= 0 {
5416                    x << (n as usize)
5417                } else {
5418                    x >> ((-n) as usize)
5419                }
5420            }
5421            binop::SHR => {
5422                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5423                if n >= 0 {
5424                    x >> (n as usize)
5425                } else {
5426                    x << ((-n) as usize)
5427                }
5428            }
5429            binop::USHR => {
5430                return Err(type_error(
5431                    "BigInts have no unsigned right shift, use >> instead",
5432                ))
5433            }
5434            _ => return Err(type_error("unsupported BigInt operation")),
5435        };
5436        Ok(self.new_bigint(r))
5437    }
5438
5439    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5440    /// being BigInt was already handled by `strict_eq`.
5441    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5442        // Order so `big` is the BigInt side and `other` the counterpart.
5443        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5444            (Some(x), _) => (x, b),
5445            (_, Some(y)) => (y, a),
5446            _ => return false,
5447        };
5448        match other {
5449            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5450            Value::Int(n) => big == num_bigint::BigInt::from(*n),
5451            Value::Float(f) => {
5452                // Equal only when the float is an integer with the same value.
5453                if !f.is_finite() || f.fract() != 0.0 {
5454                    return false;
5455                }
5456                bigint_to_f64(&big) == *f
5457            }
5458            Value::Str(s) => match parse_bigint_str(s) {
5459                Some(bs) => big == bs,
5460                None => false,
5461            },
5462            Value::Obj(_) => match self.get(other) {
5463                // A heap string parses like a primitive string.
5464                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5465                _ => {
5466                    // Other objects reduce via ToPrimitive (their string form).
5467                    let s = self.str_of(other);
5468                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5469                }
5470            },
5471            _ => false,
5472        }
5473    }
5474}
5475
5476/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5477/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5478pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5479    let t = crate::utf16::js_trim(s);
5480    if t.is_empty() {
5481        return Some(num_bigint::BigInt::from(0));
5482    }
5483    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5484        (16, h)
5485    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5486        (8, o)
5487    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5488        (2, bb)
5489    } else {
5490        (10, t)
5491    };
5492    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5493}
5494
5495/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5496/// out-of-range magnitudes become ±Infinity, matching Node.
5497pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5498    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5499        if num_traits::Signed::is_negative(b) {
5500            f64::NEG_INFINITY
5501        } else {
5502            f64::INFINITY
5503        }
5504    })
5505}
5506
5507/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5508fn js_mod(a: f64, b: f64) -> f64 {
5509    a % b
5510}
5511
5512/// Cycle bookkeeping for one `util.inspect` render.
5513///
5514/// `seen` is the chain of objects currently being rendered (an entry appearing
5515/// twice is a back-edge), and `refs` records every object a back-edge pointed
5516/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5517/// in `[Circular *N]` / `<ref *N>`.
5518/// How an array-shaped group is laid out, beyond its entries themselves.
5519#[derive(Clone, Copy)]
5520struct ArrayLayout<'a> {
5521    /// Extra own properties follow the elements, which suppresses grid grouping.
5522    has_props: bool,
5523    /// `output`'s last entry is the `... N more items` tail rather than a real
5524    /// element, so the grid must not size a column to it.
5525    has_tail: bool,
5526    /// A constructor tag printed before the brackets, with a trailing space
5527    /// (`"Uint8Array(3) "`), or empty for a plain array.
5528    base: &'a str,
5529}
5530
5531#[derive(Default)]
5532struct InspectCycles {
5533    seen: Vec<Value>,
5534    refs: Vec<Value>,
5535    /// The indent level of the value most recently EXPANDED — node's
5536    /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5537    /// while `currentDepth - thisDepth < compact`, so without it a deeply
5538    /// nested object printed on one line where node breaks the outer levels.
5539    deepest: usize,
5540}
5541
5542impl InspectCycles {
5543    /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5544    fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5545        if let Some(id) = self.id_of(h, v) {
5546            return id;
5547        }
5548        self.refs.push(v.clone());
5549        self.refs.len()
5550    }
5551
5552    /// The `*N` id already assigned to `v`, if any.
5553    fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5554        self.refs
5555            .iter()
5556            .position(|p| h.strict_eq(p, v))
5557            .map(|i| i + 1)
5558    }
5559}
5560
5561thread_local! {
5562    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5563    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5564    /// overrides it for one call, `console.log`/`util.format` use the default.
5565    /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5566    /// "already past the limit" — everything collapses to `[Object]` at the top
5567    /// level. Held as `usize` it read as an enormous depth and expanded fully.
5568    static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5569
5570    /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5571    /// group is put on one line only when the subtree below it is shallower
5572    /// than this. `compact: false` is held as 0, which no subtree depth is
5573    /// below, so every group breaks — which is exactly what node does.
5574    static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5575
5576    /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5577    /// itself passes 80.
5578    static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5579
5580    /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5581    /// order instead of insertion order. Off by default. `assert`'s diff renderer
5582    /// turns it on so that two objects built with the same keys in a different
5583    /// order diff as equal rather than as a wholesale rewrite.
5584    static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5585
5586    /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5587    /// the rest collapse into `... N more items`. Node's default is 100;
5588    /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5589    static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5590
5591    /// `util.inspect`'s `customInspect` option: whether a value's own
5592    /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5593    /// it off so a diff shows an object's real structure rather than whatever
5594    /// summary it prefers to print.
5595    static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5596
5597    /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5598    /// carries — an array's `length`, a typed array's element width and window
5599    /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5600    static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5601}
5602
5603/// Set the `util.inspect` `showHidden` option for the next render.
5604pub fn set_inspect_show_hidden(s: bool) {
5605    INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5606}
5607
5608pub(crate) fn inspect_show_hidden() -> bool {
5609    INSPECT_SHOW_HIDDEN.with(|x| x.get())
5610}
5611
5612/// Set the `util.inspect` `customInspect` option for the next render.
5613pub fn set_inspect_custom(c: bool) {
5614    INSPECT_CUSTOM.with(|x| x.set(c));
5615}
5616
5617pub(crate) fn inspect_custom() -> bool {
5618    INSPECT_CUSTOM.with(|x| x.get())
5619}
5620
5621/// Set the `util.inspect` `sorted` option for the next render.
5622pub fn set_inspect_sorted(s: bool) {
5623    INSPECT_SORTED.with(|x| x.set(s));
5624}
5625
5626pub(crate) fn inspect_sorted() -> bool {
5627    INSPECT_SORTED.with(|x| x.get())
5628}
5629
5630/// Set the `util.inspect` `maxArrayLength` for the next render.
5631pub fn set_inspect_max_array_length(n: usize) {
5632    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5633}
5634
5635pub(crate) fn inspect_max_array_length() -> usize {
5636    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5637}
5638
5639/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5640pub fn set_inspect_compact(c: i64) {
5641    INSPECT_COMPACT.with(|x| x.set(c));
5642}
5643
5644/// Set the `util.inspect` `breakLength` for the next render.
5645pub fn set_inspect_break_length(n: usize) {
5646    INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5647}
5648
5649fn inspect_compact() -> i64 {
5650    INSPECT_COMPACT.with(|x| x.get())
5651}
5652
5653/// Set the `util.inspect` depth for the next render (restore to 2 after).
5654pub fn set_inspect_max_depth(d: i64) {
5655    INSPECT_MAX_DEPTH.with(|c| c.set(d));
5656}
5657/// Twice the configured depth, which is what the inspect walk compares its
5658/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5659/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5660/// overflowed and panicked the process — an abort no script could catch.
5661fn inspect_indent_limit() -> i64 {
5662    inspect_max_depth().saturating_mul(2)
5663}
5664
5665fn inspect_max_depth() -> i64 {
5666    INSPECT_MAX_DEPTH.with(|c| c.get())
5667}
5668
5669/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5670/// reinterpret as signed.
5671///
5672/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5673/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5674/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5675/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5676/// power-of-two modulus is exact for every finite double, so this is the whole
5677/// fix — and it is the form `Math.clz32` already used.
5678pub(crate) fn to_int32(f: f64) -> i32 {
5679    to_uint32(f) as i32
5680}
5681pub(crate) fn to_uint32(f: f64) -> u32 {
5682    if !f.is_finite() {
5683        return 0;
5684    }
5685    f.trunc().rem_euclid(4294967296.0) as u32
5686}
5687
5688/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5689fn str_to_number(s: &str) -> f64 {
5690    let t = crate::utf16::js_trim(s);
5691    if t.is_empty() {
5692        return 0.0;
5693    }
5694    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5695        return i64::from_str_radix(hex, 16)
5696            .map(|n| n as f64)
5697            .unwrap_or(f64::NAN);
5698    }
5699    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5700        return i64::from_str_radix(oct, 8)
5701            .map(|n| n as f64)
5702            .unwrap_or(f64::NAN);
5703    }
5704    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5705        return i64::from_str_radix(bin, 2)
5706            .map(|n| n as f64)
5707            .unwrap_or(f64::NAN);
5708    }
5709    match t {
5710        "Infinity" | "+Infinity" => f64::INFINITY,
5711        "-Infinity" => f64::NEG_INFINITY,
5712        _ => t.parse::<f64>().unwrap_or(f64::NAN),
5713    }
5714}
5715
5716/// `util.inspect` break length (the width past which entries wrap). Node's default.
5717fn break_length() -> usize {
5718    INSPECT_BREAK_LENGTH.with(|x| x.get())
5719}
5720/// Node's default `compact` setting (the `compact * 4` column cap term).
5721/// Node's DEFAULT `compact` setting, and the initial value of
5722/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5723/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5724/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5725const DEFAULT_COMPACT: i64 = 3;
5726/// Node's default `maxArrayLength` — the initial value of
5727/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5728/// overrides per call. Read it through `inspect_max_array_length()`, never
5729/// directly: as a bare constant the option had no effect and a 120-element array
5730/// was truncated at 100 even under `maxArrayLength: Infinity`.
5731pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5732
5733/// Whether `output` fits on a single line — a faithful port of Node's
5734/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5735/// length (braces + indentation + slack).
5736fn is_below_break_length(output: &[String], start: usize) -> bool {
5737    let limit = break_length();
5738    let mut total = output.len() + start;
5739    if total + output.len() > limit {
5740        return false;
5741    }
5742    for o in output {
5743        if o.contains('\n') {
5744            return false;
5745        }
5746        total += o.chars().count();
5747        if total > limit {
5748            return false;
5749        }
5750    }
5751    true
5752}
5753
5754/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5755/// already-formatted element strings into an aligned multi-column grid. Returns
5756/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5757/// ungrouped (so the caller falls back to single-line / one-per-line).
5758fn group_array_elements(
5759    host: &JsHost,
5760    output: &[String],
5761    values: &[Value],
5762    indentation_lvl: usize,
5763    has_tail: bool,
5764) -> (Vec<String>, bool) {
5765    let separator_space = 2usize; // ", " between entries
5766                                  // A `... N more items` tail is not an element: node drops it from the grid
5767                                  // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5768                                  // re-appends it as its own final line.
5769    let output_length = output.len() - usize::from(has_tail);
5770    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5771    let mut total_length = 0usize;
5772    let mut max_length = 0usize;
5773    for &len in &data_len[..output_length] {
5774        total_length += len + separator_space;
5775        if len > max_length {
5776            max_length = len;
5777        }
5778    }
5779    let actual_max = max_length + separator_space;
5780    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5781    if !(actual_max * 3 + indentation_lvl < break_length()
5782        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5783    {
5784        return (output.to_vec(), false);
5785    }
5786    let approx_char_heights = 2.5f64;
5787    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5788    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5789    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5790    let columns = [
5791        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5792            as i64,
5793        ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5794        inspect_compact().saturating_mul(4),
5795        15,
5796    ]
5797    .into_iter()
5798    .min()
5799    .unwrap();
5800    if columns <= 1 {
5801        return (output.to_vec(), false);
5802    }
5803    let columns = columns as usize;
5804    // The widest entry (plus separator) in each column.
5805    let mut max_line_length = vec![0usize; columns];
5806    for (i, slot) in max_line_length.iter_mut().enumerate() {
5807        let mut line_length = 0;
5808        let mut j = i;
5809        while j < output_length {
5810            if data_len[j] > line_length {
5811                line_length = data_len[j];
5812            }
5813            j += columns;
5814        }
5815        *slot = line_length + separator_space;
5816    }
5817    // Right-align (padStart) only when every element is a number/bigint.
5818    let pad_start = values.iter().all(|v| {
5819        matches!(v, Value::Int(_) | Value::Float(_))
5820            || matches!(host.get(v), Some(JsObj::BigInt(_)))
5821    });
5822    let mut tmp = Vec::new();
5823    let mut i = 0;
5824    while i < output_length {
5825        let max = (i + columns).min(output_length);
5826        let mut str_line = String::new();
5827        let mut j = i;
5828        while j < max.saturating_sub(1) {
5829            // `output[j]` has no colors here, so padding == max_line_length[col].
5830            let col = j - i;
5831            let cell = format!("{}, ", output[j]);
5832            let target = max_line_length[col];
5833            str_line.push_str(&pad_to(&cell, target, pad_start));
5834            j += 1;
5835        }
5836        // The last cell of the row: right-aligned entries pad without the ", ".
5837        if pad_start {
5838            let col = j - i;
5839            let target = max_line_length[col] - separator_space;
5840            str_line.push_str(&pad_to(&output[j], target, true));
5841        } else {
5842            str_line.push_str(&output[j]);
5843        }
5844        tmp.push(str_line);
5845        i += columns;
5846    }
5847    if has_tail {
5848        tmp.push(output[output_length].clone());
5849    }
5850    (tmp, true)
5851}
5852
5853/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5854/// (Padding is measured in chars; already ANSI-free here.)
5855fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5856    let len = s.chars().count();
5857    if len >= width {
5858        return s.to_string();
5859    }
5860    let fill = " ".repeat(width - len);
5861    if pad_start {
5862        format!("{fill}{s}")
5863    } else {
5864        format!("{s}{fill}")
5865    }
5866}
5867
5868/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5869/// `lib/internal/util/inspect.js`.
5870///
5871/// The quote character is chosen so the contents need as little escaping as
5872/// possible: single quotes normally, double quotes when the string contains a
5873/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5874/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5875/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5876/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5877fn quote_str(s: &str) -> String {
5878    let quote = if !s.contains('\'') {
5879        '\''
5880    } else if !s.contains('"') {
5881        '"'
5882    } else if !s.contains('`') && !s.contains("${") {
5883        '`'
5884    } else {
5885        '\''
5886    };
5887    let mut out = String::with_capacity(s.len() + 2);
5888    out.push(quote);
5889    for c in s.chars() {
5890        match c {
5891            _ if c == quote => {
5892                out.push('\\');
5893                out.push(c);
5894            }
5895            '\\' => out.push_str("\\\\"),
5896            '\u{8}' => out.push_str("\\b"),
5897            '\t' => out.push_str("\\t"),
5898            '\n' => out.push_str("\\n"),
5899            '\u{c}' => out.push_str("\\f"),
5900            '\r' => out.push_str("\\r"),
5901            '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5902            _ => out.push(c),
5903        }
5904    }
5905    out.push(quote);
5906    out
5907}
5908
5909/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5910fn fmt_key(k: &str) -> String {
5911    let ok = !k.is_empty()
5912        && k.chars()
5913            .next()
5914            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5915            .unwrap_or(false)
5916        && k.chars()
5917            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5918    if ok {
5919        k.to_string()
5920    } else {
5921        quote_str(k)
5922    }
5923}
5924
5925// ── iteration ────────────────────────────────────────────────────────────────
5926
5927impl JsHost {
5928    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5929    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5930    /// holds no host borrow across resumes.
5931    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5932        match self.get(v) {
5933            Some(JsObj::Array(items)) => Ok(items.clone()),
5934            Some(JsObj::Str(s)) => {
5935                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5936                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5937            }
5938            Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
5939            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5940            Some(JsObj::Map { entries, .. }) => {
5941                // Map iterates as `[key, value]` pairs.
5942                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5943                Ok(pairs
5944                    .into_iter()
5945                    .map(|(k, v)| self.new_array(vec![k, v]))
5946                    .collect())
5947            }
5948            // A `Buffer` iterates over its BYTES and a typed array over its
5949            // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5950            // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5951            // "object is not iterable", which is the same invariant holding at
5952            // one of its two sites.
5953            Some(JsObj::Object(props))
5954                if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
5955            {
5956                // Iterating a view over a DETACHED buffer throws, naming the
5957                // `values` iterator — reading its elements answers zero length,
5958                // but spreading it is a method call and does not.
5959                if crate::stdlib::typedarray::view_detached_h(self, v) {
5960                    return Err(crate::stdlib::typedarray::detached_error(
5961                        "%TypedArray%.prototype",
5962                        "values",
5963                        false,
5964                    ));
5965                }
5966                Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
5967            }
5968            // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
5969            // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
5970            // produced `number is not iterable`, which no engine emits.
5971            _ => {
5972                let shown = self.inspect(v);
5973                Err(type_error(&format!("{shown} is not iterable")))
5974            }
5975        }
5976    }
5977
5978    /// Enumerable string keys of an object/array (for `for-in`). Internal
5979    /// symbol-keyed props (`@@…`) are not enumerable.
5980    /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
5981    /// not already seen, walking the whole prototype chain. Class methods and the
5982    /// builtin prototypes are non-enumerable, so in practice this only surfaces
5983    /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
5984    /// is exactly the constructor-function idiom older packages are written in.
5985    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
5986        let mut keys = self.own_enum_key_names(v);
5987        let mut cur = self.proto_of(v);
5988        let mut hops = 0;
5989        while let Some(p) = cur {
5990            // A cyclic or pathologically deep chain must not hang the loop.
5991            hops += 1;
5992            if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
5993                break;
5994            }
5995            for k in self.own_enum_key_names(&p) {
5996                if !keys.contains(&k) {
5997                    keys.push(k);
5998                }
5999            }
6000            cur = self.proto_of(&p);
6001        }
6002        keys.into_iter().map(|k| self.new_str(k)).collect()
6003    }
6004
6005    /// The own *enumerable* string keys of `v`, in property order — the single
6006    /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6007    /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6008    /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6009    /// `prop_attrs` are excluded.
6010    pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6011        self.own_key_names(v, true)
6012    }
6013
6014    /// Own string keys of `v` in insertion order. `enum_only` drops the
6015    /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6016    /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6017    pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6018        let mut keys = self.own_enum_data_keys(v, enum_only);
6019        // A global a SCRIPT created (`x = 1` with no declaration) is an own
6020        // ENUMERABLE property of the global object, but lives in the globals map
6021        // rather than in its property map — so no listing saw it, while
6022        // `globalThis.x` read it back and its descriptor called it enumerable.
6023        if self.is_global_object(v) {
6024            for k in self.globals.keys() {
6025                if !keys.contains(k) {
6026                    keys.push(k.clone());
6027                }
6028            }
6029        }
6030        // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6031        // `RegExpObj` struct, not a property map — so nothing above can list it.
6032        // Non-enumerable, so only `getOwnPropertyNames` sees it.
6033        if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6034            keys.push("lastIndex".to_string());
6035        }
6036        // An accessor defined before its object had any ordering marker (a class
6037        // prototype accessor, say) still has to appear.
6038        for k in self.own_accessor_keys(v) {
6039            if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6040                keys.push(k);
6041            }
6042        }
6043        keys
6044    }
6045
6046    /// The keys that own a slot in the object's property map, in insertion
6047    /// order, resolving accessor ordering markers back to their real key.
6048    /// Every global a SCRIPT created, in creation order — the own enumerable
6049    /// keys of the global object that live in the globals map rather than in
6050    /// its property map. `x = 1` with no declaration makes one, and
6051    /// `Object.keys(globalThis)` reports it in node.
6052    pub fn script_global_names(&self) -> Vec<String> {
6053        self.globals.keys().cloned().collect()
6054    }
6055    /// Drop a global a script created. Reports whether it was there.
6056    pub fn remove_global(&mut self, name: &str) -> bool {
6057        self.globals.shift_remove(name).is_some()
6058    }
6059    fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6060        match self.get(v) {
6061            // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6062            // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6063            // the `length`/`byteLength` view metadata, which V8 keeps on the
6064            // prototype chain or as non-enumerable own slots.
6065            // A `Buffer` and every other typed array are index-keyed exotics:
6066            // their own enumerable keys are `"0".."len-1"` (the elements live in
6067            // a hidden slot), never the `length`/`byteLength` view metadata,
6068            // which V8 keeps on the prototype chain or as non-enumerable own
6069            // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6070            // and `JSON.stringify(u8)` was `{}` where node gives
6071            // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6072            // the two views of the same question disagreed.
6073            Some(JsObj::Object(props))
6074                if matches!(
6075                    props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6076                    Some("Buffer") | Some("TypedArray")
6077                ) =>
6078            {
6079                // A view over a DETACHED buffer has no index properties at all:
6080                // its own `length` still holds the old count, so reading that
6081                // back left `Object.keys` listing eight names over no bytes.
6082                if crate::stdlib::typedarray::view_detached_h(self, v) {
6083                    return Vec::new();
6084                }
6085                // A Buffer counts its byte store; every other view reports the
6086                // element count of its window onto the ArrayBuffer.
6087                let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6088                    Some(JsObj::Array(items)) => items.len(),
6089                    _ => props
6090                        .get("length")
6091                        .map(|l| self.to_number(l))
6092                        .unwrap_or(0.0) as usize,
6093                };
6094                (0..n).map(|i| i.to_string()).collect()
6095            }
6096            Some(JsObj::Object(props)) => props
6097                .keys()
6098                .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6099                    Some(real) => Some(real.to_string()),
6100                    None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6101                    None => None,
6102                })
6103                .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6104                .collect(),
6105            // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6106            // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6107            // Without this arm every whole-object view of a string primitive was
6108            // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6109            // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6110            // `'ab'.length` answered normally, so the two views disagreed. The
6111            // spread form `{...'ab'}` went through a different path and was
6112            // already right, which is what made the gap easy to miss.
6113            Some(JsObj::Str(s)) => {
6114                let mut keys: Vec<String> =
6115                    (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6116                if !enum_only {
6117                    keys.push("length".into());
6118                }
6119                keys
6120            }
6121            // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6122            // ascending, then the exotic non-enumerable `length`, then the
6123            // ordinary string keys in insertion order. Those ordinary keys have
6124            // no property map to live in — a `str.match()` result's
6125            // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6126            // in the fn-prop side table — so they are read back from there.
6127            Some(JsObj::Array(items)) => {
6128                // An ELIDED element is not an own property at all, so it
6129                // contributes no key — the difference behind
6130                // `Object.keys([1,,3])` being `['0','2']`.
6131                let mut keys: Vec<String> = (0..items.len())
6132                    .filter(|i| !self.is_hole(v, *i))
6133                    .map(|i| i.to_string())
6134                    .collect();
6135                if !enum_only {
6136                    keys.push("length".into());
6137                }
6138                keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6139                    !k.starts_with("@@")
6140                        && !k.starts_with('#')
6141                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6142                }));
6143                keys
6144            }
6145            // A function/class keeps every own property in the side table. Its
6146            // exotic `name`/`length`/`prototype` and its class methods are all
6147            // non-enumerable, so under `enum_only` what is left is exactly what
6148            // a script assigned; `getOwnPropertyNames` reports the exotics too,
6149            // in V8's order (`length`, `name`, `prototype`, then the rest).
6150            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6151                let mut keys: Vec<String> = Vec::new();
6152                if !enum_only {
6153                    keys.push("length".into());
6154                    keys.push("name".into());
6155                    if self.owns_prototype(v) {
6156                        keys.push("prototype".into());
6157                    }
6158                }
6159                let rest: Vec<String> = self
6160                    .fn_prop_keys(v)
6161                    .into_iter()
6162                    // An accessor's ordering marker resolves back to its real
6163                    // key, so a static getter enumerates where it was declared.
6164                    .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6165                        Some(real) => Some(real.to_string()),
6166                        None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6167                        None => None,
6168                    })
6169                    .filter(|k| {
6170                        !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6171                    })
6172                    .collect();
6173                keys.extend(rest);
6174                keys
6175            }
6176            // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6177            // members node-js implements, so a package that copies a namespace
6178            // key-by-key gets the working set instead of an empty object.
6179            Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6180            // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6181            // slots, so what a script assigned lives in the side table — and is
6182            // just as much an own property as an object's.
6183            Some(_) => self
6184                .fn_prop_keys(v)
6185                .into_iter()
6186                .filter(|k| {
6187                    !k.starts_with("@@")
6188                        && !k.starts_with('#')
6189                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6190                })
6191                .collect(),
6192            _ => Vec::new(),
6193        }
6194    }
6195
6196    /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6197    /// through the byte store; everything else reads the property map. Own
6198    /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6199    /// their getters, which cannot happen under the host borrow.
6200    pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6201        self.own_enum_key_names(v)
6202            .into_iter()
6203            .map(|k| {
6204                let val = match self.get(v) {
6205                    // A Buffer's index keys read out of the hidden `@@bytes`
6206                    // array; resolve inline rather than through
6207                    // `buffer::byte_get`, which would re-borrow the host.
6208                    Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6209                        // A Buffer's elements live in `@@bytes` and every
6210                        // other typed array's in `@@elems`; both are index
6211                        // keys with no entry in the property map.
6212                        match k.parse::<usize>() {
6213                            Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6214                                .get(i)
6215                                .cloned()
6216                                .unwrap_or(Value::Undef),
6217                            _ => Value::Undef,
6218                        }
6219                    }),
6220                    // A Map/Set/Promise/RegExp/generator keeps every own
6221                    // property in the side table.
6222                    Some(
6223                        JsObj::Map { .. }
6224                        | JsObj::Set { .. }
6225                        | JsObj::Promise { .. }
6226                        | JsObj::RegExp(_)
6227                        | JsObj::Generator { .. }
6228                        | JsObj::Symbol { .. }
6229                        | JsObj::BigInt(_)
6230                        | JsObj::Iter { .. },
6231                    ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6232                    // An index reads the element; any other own key (`foo`,
6233                    // a match result's `index`) lives in the side table.
6234                    Some(JsObj::Array(items)) => k
6235                        .parse::<usize>()
6236                        .ok()
6237                        .and_then(|i| items.get(i).cloned())
6238                        .or_else(|| self.fn_prop(v, &k))
6239                        .unwrap_or(Value::Undef),
6240                    Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6241                        self.fn_prop(v, &k).unwrap_or(Value::Undef)
6242                    }
6243                    _ => Value::Undef,
6244                };
6245                (k, val)
6246            })
6247            .collect()
6248    }
6249}
6250
6251/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6252/// accessor's getter invoked — the observable shape `Object.values`,
6253/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6254/// outside a `with_host` borrow because a getter re-enters the host.
6255pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6256    // A Proxy has no property map at all: its own enumerable entries come from
6257    // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6258    // throws surfaces as an empty result here because this signature is
6259    // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6260    // and friends) go through `builtins::object_keys`, which does.
6261    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6262        return crate::proxy::own_enum_entries(v);
6263    }
6264    // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6265    // all: its members are resolved on demand by `namespace_property`, which
6266    // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6267    // Without this, spread and `Object.assign` copied the namespace's KEYS with
6268    // `undefined` for every value — measured against node v26.7.0,
6269    // `{...require('path')}.join` was `undefined` here and a function there,
6270    // while `Object.entries(require('path'))` (which resolves through
6271    // `builtins`, not through this borrow) was already correct. Two enumeration
6272    // paths, one of them silently value-less.
6273    if let Some(ns) = with_host(|h| match h.get(v) {
6274        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6275        _ => None,
6276    }) {
6277        return Ok(with_host(|h| h.own_enum_key_names(v))
6278            .into_iter()
6279            .map(|k| {
6280                let val = crate::builtins::namespace_property(&ns, &k);
6281                (k, val)
6282            })
6283            .collect());
6284    }
6285    // A string primitive's own entries are its code units. `own_enum_entries`
6286    // cannot build them: allocating the one-character string for each index
6287    // needs `&mut` host access, and it runs under a shared borrow.
6288    if let Some(sv) = with_host(|h| match h.get(v) {
6289        Some(JsObj::Str(s)) => Some(s.clone()),
6290        _ => None,
6291    }) {
6292        let units = crate::utf16::Units::of(&sv);
6293        return Ok(with_host(|h| {
6294            (0..units.len())
6295                .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6296                .collect()
6297        }));
6298    }
6299    let accessor_keys: Vec<String> = with_host(|h| {
6300        h.own_accessor_keys(v)
6301            .into_iter()
6302            .filter(|k| h.prop_attrs(v, k).enumerable)
6303            .collect()
6304    });
6305    let entries = with_host(|h| h.own_enum_entries(v));
6306    // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6307    // object spread and `JSON.stringify` all read through here, and every one
6308    // of them swallowed the exception and reported the property as absent (or
6309    // as `null`) instead.
6310    let mut out = Vec::with_capacity(entries.len());
6311    for (k, val) in entries {
6312        if accessor_keys.contains(&k) {
6313            out.push((k.clone(), get_prop_chain(v, &k)?));
6314        } else {
6315            out.push((k, val));
6316        }
6317    }
6318    Ok(out)
6319}
6320
6321// ── function invocation ──────────────────────────────────────────────────────
6322
6323/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6324/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6325/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6326/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6327/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6328/// Float→i64/f64 per the export signature).
6329fn marshal_ffi_arg(v: &Value) -> Value {
6330    match v {
6331        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6332            Some(s) => Value::str(s),
6333            None => v.clone(),
6334        },
6335        _ => v.clone(),
6336    }
6337}
6338
6339/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6340pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6341    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6342    // line)`; compile + register the block's exported functions, returning JS
6343    // `undefined` (`Value::Undef`).
6344    if name == "__rust_compile" {
6345        let b64 = args
6346            .first()
6347            .map(|v| with_host(|h| h.str_of(v)))
6348            .unwrap_or_default();
6349        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6350    }
6351    if let Some(v) = with_host(|h| h.read_name(name)) {
6352        return invoke(&v, args, None);
6353    }
6354    // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6355    // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6356    // and `[eval][0](src)` all reach the same function value but are INDIRECT
6357    // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6358    // This is the one place the two forms are distinguishable without a compiler
6359    // change: `call_named` is reached only from `ops::CALL`, which the compiler
6360    // emits exclusively for a bare-identifier callee, while every value-call form
6361    // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6362    // has already established that `eval` is not shadowed by a user binding.
6363    if name == "eval" {
6364        return crate::builtins::eval_source(args.first(), true);
6365    }
6366    if crate::builtins::is_known_builtin(name) {
6367        return crate::builtins::call_builtin_function(name, args);
6368    }
6369    // A `rust { ... }` block's exported functions are callable by bareword.
6370    // Reached only after user names/globals and builtins all miss, so JS code
6371    // always wins; the registry membership check keeps this off the hot path.
6372    if fusevm::ffi::is_registered(name) {
6373        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6374        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6375            return r;
6376        }
6377    }
6378    Err(ref_error(name))
6379}
6380
6381thread_local! {
6382    /// The constructor a builtin STATIC is currently being invoked on.
6383    ///
6384    /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6385    /// builtin, which is reached by NAME and so cannot see `A`. The species
6386    /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6387    /// their result with `this`, so on a subclass they must construct through
6388    /// it. A stack, since one static can call another.
6389    static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6390        const { std::cell::RefCell::new(Vec::new()) };
6391}
6392
6393/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6394pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6395    STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6396    let out = f();
6397    STATIC_THIS.with(|s| {
6398        s.borrow_mut().pop();
6399    });
6400    out
6401}
6402
6403/// The constructor the running builtin static was called on, if it was reached
6404/// through a subclass rather than directly.
6405pub fn current_static_this() -> Option<Value> {
6406    STATIC_THIS.with(|s| s.borrow().last().cloned())
6407}
6408
6409/// `recv.name(args)`.
6410pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6411    // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6412    // the failure is the property read, not the call: node reports
6413    // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6414    // whole method dispatch against the nullish receiver, found nothing, and
6415    // reported `undefined.foo is not a function` — the wrong error class of
6416    // message for the single most common runtime fault in JS, and one that
6417    // points at the callee instead of at the base that was nullish.
6418    if with_host(|h| h.is_nullish(recv)) {
6419        return Err(type_error(&format!(
6420            "Cannot read properties of {} (reading '{name}')",
6421            with_host(|h| h.str_of(recv))
6422        )));
6423    }
6424    // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6425    // comes first: an unbranded receiver throws here rather than reporting the
6426    // method missing. Only a `#`-prefixed name pays the extra probe.
6427    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6428        return Err(crate::builtins::private_brand_message(name, false));
6429    }
6430    // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6431    // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6432    // below all read a property map a proxy does not have.
6433    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6434        let f = crate::builtins::get_property(recv, name)?;
6435        if !with_host(|h| is_callable(h, &f)) {
6436            return Err(type_error(&format!("{name} is not a function")));
6437        }
6438        // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6439        // `Object.prototype` methods are generic over `this`. node-js models each
6440        // as a thunk BOUND to the object it was read off — through a proxy, that
6441        // is the target — so invoking the thunk answers for the target and skips
6442        // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6443        // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6444        // those against the PROXY, which is the `this` the real method receives.
6445        //
6446        // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6447        // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6448        // an array stringifies `1,2` through `Array.prototype.toString`, not
6449        // `[object Object]`), which the bound thunk already gets right.
6450        if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6451            if with_host(|h| is_callable(h, recv)) {
6452                if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6453                    return Ok(r);
6454                }
6455            }
6456            if matches!(
6457                name,
6458                "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6459            ) {
6460                return crate::builtins::object_builtin_method(recv, name, args);
6461            }
6462            // The three above resolve by the TARGET's kind, and the thunk is
6463            // already bound to the target — so it must be invoked WITHOUT a
6464            // receiver override. Passing the proxy as `this` made the
6465            // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6466            // call straight back into this branch, so `String(new Proxy({}, {}))`
6467            // recursed until the stack overflowed and the process aborted.
6468            if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6469                return invoke(&f, args, None);
6470            }
6471        }
6472        return invoke(&f, args, Some(recv.clone()));
6473    }
6474    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6475    // name.
6476    if let Some(ns) = with_host(|h| match h.get(recv) {
6477        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6478        _ => None,
6479    }) {
6480        let qualified = format!("{ns}.{name}");
6481        if crate::builtins::is_known_builtin(&qualified) {
6482            return crate::builtins::call_builtin_function(&qualified, args);
6483        }
6484    }
6485    // Object / instance: an accessor getter that yields a function, an own or
6486    // inherited method (class methods live on the prototype chain), then an
6487    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6488    // directly — NOT get_property — so the Object.prototype-builtin fallback
6489    // never routes back through a BoundMethod and recurses.
6490    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6491        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6492        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6493        // A user-added or reparented-prototype method takes precedence over the
6494        // native dispatcher — matching JS resolution order (own → prototype
6495        // chain). This is what lets Express work: it does
6496        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6497        // where `send` is a plain function on the reparented prototype. Native
6498        // instance methods (`res.end`/`write`/…) are NOT stored as plain
6499        // function properties, so `lookup_chain` misses them and we fall through
6500        // to `instance_call` for the real native behavior.
6501        if let Some(tag) = crate::stdlib::native_tag(recv) {
6502            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6503                if with_host(|h| is_callable(h, &f)) {
6504                    return invoke(&f, args, Some(recv.clone()));
6505                }
6506            }
6507            // `Object.prototype` methods reach a native instance too — a Buffer
6508            // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6509            // chain, and the native dispatcher has no entry for them.
6510            if crate::builtins::is_object_builtin_method(name)
6511                && !crate::stdlib::instance_has_method(&tag, name)
6512            {
6513                return crate::builtins::object_builtin_method(recv, name, args);
6514            }
6515            return crate::stdlib::instance_call(&tag, recv, name, args);
6516        }
6517        // A primitive wrapper forwards to the primitive's method table, the
6518        // same way a native instance forwards to its tag's. A user method on
6519        // the wrapper or anywhere on its chain still wins first.
6520        if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6521            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6522                if with_host(|h| is_callable(h, &f)) {
6523                    return invoke(&f, args, Some(recv.clone()));
6524                }
6525            }
6526            // The reflective `Object.prototype` methods answer for the WRAPPER
6527            // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6528            // properties, not about the string.
6529            if crate::builtins::is_object_builtin_method(name) {
6530                return crate::builtins::object_builtin_method(recv, name, args);
6531            }
6532            return call_method(&prim, name, args);
6533        }
6534        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6535            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6536            if with_host(|h| is_callable(h, &f)) {
6537                return invoke(&f, args, Some(recv.clone()));
6538            }
6539        }
6540        // A Proxy in the prototype chain serves the method through its `get`
6541        // trap. `lookup_chain` below reads property maps, which a proxy has none
6542        // of, so without this `child.m()` on `Object.create(proxy)` reported
6543        // "m is not a function" even though `child.m` already read correctly.
6544        if crate::builtins::proxy_proto_link(recv, name).is_some() {
6545            let f = crate::builtins::get_property(recv, name)?;
6546            if !with_host(|h| is_callable(h, &f)) {
6547                return Err(type_error(&format!("{name} is not a function")));
6548            }
6549            return invoke(&f, args, Some(recv.clone()));
6550        }
6551        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6552            if with_host(|h| is_callable(h, &f)) {
6553                return invoke(&f, args, Some(recv.clone()));
6554            }
6555            return Err(type_error(&format!("{name} is not a function")));
6556        }
6557        // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6558        // it: a plain object is not LINKED to the intrinsic prototype object,
6559        // its `Object.prototype` members are synthesized instead. So
6560        // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6561        // while `({}).tap` already read back as `f`.
6562        if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6563            if with_host(|h| is_callable(h, &f)) {
6564                return invoke(&f, args, Some(recv.clone()));
6565            }
6566        }
6567        // A method from an intrinsic prototype this object's CHAIN passes
6568        // through — `Object.create(Array.prototype).push(1)`. The read already
6569        // resolves it through the same owner oracle; dispatch reported "is not
6570        // a function", the read and the call disagreeing once more.
6571        if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6572            if owner != "Object" {
6573                return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6574            }
6575        }
6576        if crate::builtins::is_object_builtin_method(name) {
6577            return crate::builtins::object_builtin_method(recv, name, args);
6578        }
6579        if name == "constructor" {
6580            if let Some(r) = call_default_ctor(recv, &args) {
6581                return r;
6582            }
6583        }
6584        return Err(type_error(&format!("{name} is not a function")));
6585    }
6586    // Function value methods: call / apply / bind, then any static method stored
6587    // on the function object.
6588    if matches!(
6589        with_host(|h| h.kind_of(recv)),
6590        Some(ObjKind::Func)
6591            | Some(ObjKind::Class)
6592            | Some(ObjKind::BoundFunc)
6593            | Some(ObjKind::BoundMethod)
6594            | Some(ObjKind::Builtin)
6595    ) {
6596        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6597            return Ok(r);
6598        }
6599        // A static method (own or inherited): `this` is the constructor (`recv`).
6600        let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6601            with_host(|h| h.class_static(recv, name))
6602        } else {
6603            with_host(|h| h.fn_prop(recv, name))
6604        };
6605        if let Some(f) = stat {
6606            if with_host(|h| is_callable(h, &f)) {
6607                return invoke(&f, args, Some(recv.clone()));
6608            }
6609        }
6610        // `class_static` only walks user-class `extends` links, so a chain that
6611        // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6612        // could not reach that builtin's statics: `D.from([1,2])` threw
6613        // "from is not a function" even though `typeof D.from` said `function`.
6614        // Re-dispatch the call against that ancestor, which is what reaches a
6615        // builtin namespace's methods.
6616        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6617            if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6618                if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6619                    // The subclass is recorded so a species-aware static
6620                    // (`Array.from`, `Promise.resolve`, …) builds its result
6621                    // through it rather than through the builtin.
6622                    return with_static_this(recv, || call_method(&anc, name, args));
6623                }
6624            }
6625        }
6626        // A method inherited via the function's [[Prototype]] chain (set with
6627        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6628        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6629        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6630            if with_host(|h| is_callable(h, &f)) {
6631                return invoke(&f, args, Some(recv.clone()));
6632            }
6633        }
6634        // An `Object.prototype` method invoked with a builtin namespace/prototype
6635        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6636        // ownership probe) — dispatch it against the builtin receiver.
6637        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6638            && crate::builtins::is_object_builtin_method(name)
6639        {
6640            return crate::builtins::object_builtin_method(recv, name, args);
6641        }
6642    }
6643    if name == "constructor" {
6644        if let Some(r) = call_default_ctor(recv, &args) {
6645            return r;
6646        }
6647    }
6648    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6649    crate::builtins::call_type_method(recv, name, args)
6650}
6651
6652/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6653/// owns a `constructor` slot.
6654///
6655/// Reading the property already resolves a builtin instance's native constructor
6656/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6657/// consulted the prototype chain, so the two disagreed:
6658/// `(function(){}).constructor === Function` read `true` while
6659/// `(function(){}).constructor('return 9')` threw
6660/// `TypeError: constructor is not a function`. That call form is exactly how
6661/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6662/// constructor. Resolved here through the same one definition the read uses, so
6663/// the two can no longer drift apart. `None` means "not resolvable/callable",
6664/// leaving the caller's original error in place.
6665fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6666    let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6667    with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6668}
6669
6670/// Call any callable value.
6671pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6672    // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6673    // Probed by kind first so the ordinary call path never clones its arguments.
6674    if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6675        return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6676    }
6677    let obj = with_host(|h| h.get(callable).cloned());
6678    match obj {
6679        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6680        // against the invoke-time `this` (supplied by `.call`/`.apply`).
6681        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6682            let recv = this.unwrap_or(Value::Undef);
6683            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6684        }
6685        // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6686        // form a library uses to read a slot from an arbitrary receiver
6687        // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6688        // .call(m)`). It brand-checks `this` and reads, or throws naming
6689        // itself.
6690        // The setter half of the `arguments`/`caller` poison pill — the only
6691        // intrinsic accessor here that has one, and it throws like its getter.
6692        Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6693            let _ = &name;
6694            let recv = this.unwrap_or(Value::Undef);
6695            // The setter half accepts silently for the same receivers the
6696            // getter answers for, and throws for the rest.
6697            if with_host(|h| h.fn_is_sloppy(&recv)) {
6698                Ok(Value::Undef)
6699            } else {
6700                Err(type_error(crate::builtins::POISON_PILL))
6701            }
6702        }
6703        Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6704            let recv = this.unwrap_or(Value::Undef);
6705            let rest = &name["@protoget:".len()..];
6706            let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6707            crate::builtins::proto_getter_call(ctor, key, &recv)
6708        }
6709        // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6710        // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6711        // this:
6712        //
6713        //     function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6714        //     InternalDecoder.prototype = StringDecoder.prototype;
6715        //
6716        // A native constructor builds a fresh tagged object, so initializing the
6717        // SUPPLIED object means building one and moving its slots across.
6718        //
6719        // The guard is deliberately narrow: `obj` must already inherit from THIS
6720        // constructor's prototype, i.e. the subclass really did adopt it. Without
6721        // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6722        // return a string / a buffer — would start mutating `x` instead.
6723        Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6724            let target = this.expect("guard checked");
6725            let built = crate::stdlib::construct(name, &args)
6726                .expect("guard checked a native constructor")?;
6727            adopt_native_slots(&target, &built);
6728            Ok(Value::Undef)
6729        }
6730        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6731        Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6732        // A method read off an object is modelled as a thunk BOUND to it, but an
6733        // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6734        // rebinds `this`, and every `Array.prototype` method is generic over it, so
6735        // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6736        // override made that read back as the empty array the thunk was read off.
6737        // A nullish override is ignored: it carries no receiver to dispatch on.
6738        Some(JsObj::BoundMethod { recv, name }) => {
6739            let target = match &this {
6740                Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6741                _ => &recv,
6742            };
6743            // A thunk read off an ARRAY carries an `Array.prototype` method, and
6744            // those are generic over `this` — route the rebound call through
6745            // `proto_method` so an array-LIKE receiver takes the generic path
6746            // instead of being told the method does not exist.
6747            if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6748                return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6749            }
6750            call_method(target, &name, args)
6751        }
6752        Some(JsObj::BoundFunc {
6753            target,
6754            this: bthis,
6755            args: pre,
6756        }) => {
6757            let mut all = pre;
6758            all.extend(args);
6759            invoke(&target, all, Some(bthis))
6760        }
6761        Some(JsObj::Class(c)) => Err(type_error(&format!(
6762            "Class constructor {} cannot be invoked without 'new'",
6763            c.name
6764        ))),
6765        _ => Err(type_error(&format!(
6766            "{} is not a function",
6767            with_host(|h| h.str_of(callable))
6768        ))),
6769    }
6770}
6771
6772/// Whether calling the native constructor `name` with `this` is the ES5
6773/// constructor-stealing pattern rather than an ordinary call.
6774///
6775/// True only when `name` really is a native stdlib constructor AND `this` is a
6776/// plain object that already inherits from that constructor's prototype — the
6777/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6778/// object that merely happens to be passed as `this` does not qualify, so
6779/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6780fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6781    let Some(target) = this else { return false };
6782    if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6783        return false;
6784    }
6785    // Already initialized (e.g. a re-entrant call) — nothing to steal.
6786    if crate::stdlib::native_tag(target).is_some() {
6787        return false;
6788    }
6789    let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6790        return false;
6791    };
6792    let mut cur = with_host(|h| h.proto_of(target));
6793    while let Some(p) = cur {
6794        if p == proto {
6795            return true;
6796        }
6797        cur = with_host(|h| h.proto_of(&p));
6798    }
6799    false
6800}
6801
6802/// Move a freshly-constructed native instance's state onto `target`, so an
6803/// object built by a subclass constructor becomes a working instance of the
6804/// native class. Copies every own key the native constructor set — the hidden
6805/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6806/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6807fn adopt_native_slots(target: &Value, built: &Value) {
6808    let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6809        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6810        _ => Vec::new(),
6811    });
6812    with_host(|h| {
6813        if let Some(JsObj::Object(p)) = h.get_mut(target) {
6814            for (k, v) in slots {
6815                p.insert(k, v);
6816            }
6817        }
6818    });
6819}
6820
6821/// Execute a user function/closure body on a fresh frame.
6822pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6823    run_user_func_of(fv, args, this, None)
6824}
6825
6826/// [`run_user_func`] with the function VALUE the call came through, which the
6827/// `arguments` object needs for its `callee`.
6828pub fn run_user_func_of(
6829    fv: &FuncVal,
6830    args: Vec<Value>,
6831    this: Option<Value>,
6832    callee: Option<Value>,
6833) -> Result<Value, String> {
6834    run_user_func_full(fv, args, this, None, callee)
6835}
6836
6837/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6838pub fn run_user_func_nt(
6839    fv: &FuncVal,
6840    args: Vec<Value>,
6841    this: Option<Value>,
6842    new_target: Option<Value>,
6843) -> Result<Value, String> {
6844    run_user_func_full(fv, args, this, new_target, None)
6845}
6846
6847fn run_user_func_full(
6848    fv: &FuncVal,
6849    args: Vec<Value>,
6850    this: Option<Value>,
6851    new_target: Option<Value>,
6852    callee: Option<Value>,
6853) -> Result<Value, String> {
6854    // Consumed first, before anything here can start another call.
6855    let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6856    // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6857    // the entire compiled body, `sub_chunks` and all — on every single call.
6858    // The chunk is now reached once per pooled VM, in the two arms below.
6859    let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6860        let d = &h.funcs[fv.def_id];
6861        (
6862            d.params.clone(),
6863            d.is_generator,
6864            d.is_async,
6865            d.is_arrow,
6866            d.name.clone(),
6867        )
6868    });
6869    let env = new_env(fv.env.clone());
6870    // Bind the simple/rest arg slots; destructuring + defaults run in the body
6871    // prologue (compiled ahead of the user statements).
6872    let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6873    bind_params(
6874        &env,
6875        &params,
6876        args,
6877        is_arrow_def,
6878        callee.as_ref(),
6879        fn_is_sloppy && !is_arrow_def,
6880    );
6881    // Arrow functions capture `this` lexically; regular functions receive it.
6882    let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6883    // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6884    // becomes the global object. Only a strict function keeps `undefined`, and
6885    // an arrow has no `this` of its own to substitute. Leaving it undefined
6886    // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6887    // saw `undefined` where node sees `globalThis`.
6888    let sloppy_this = !fv.is_arrow
6889        && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6890        && match &this_val {
6891            None => true,
6892            Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6893        };
6894    if sloppy_this {
6895        this_val = Some(with_host(|h| h.global_object()));
6896    } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6897        // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6898        // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6899        // wrapper rather than the number. Only strict mode passes it through.
6900        if let Some(t) = this_val.clone() {
6901            let boxed = crate::builtins::to_object(&t);
6902            this_val = Some(boxed);
6903        }
6904    }
6905    // A generator function does not run its body on call — it returns a suspended
6906    // generator over the already-bound frame.
6907    if is_generator {
6908        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6909        let gen = make_generator(
6910            chunk,
6911            env,
6912            this_val,
6913            fv.home_class.clone(),
6914            fv.home_static,
6915            fv.home_object.clone(),
6916            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6917        );
6918        if is_async {
6919            if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6920                with_host(|h| h.generators[id as usize].async_gen = true);
6921            }
6922        }
6923        return Ok(gen);
6924    }
6925    // An async function runs on a coroutine and returns a Promise: it executes
6926    // synchronously up to the first `await`, then continues via microtasks.
6927    if is_async {
6928        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6929        let gen = make_generator(
6930            chunk,
6931            env,
6932            this_val,
6933            fv.home_class.clone(),
6934            fv.home_static,
6935            fv.home_object.clone(),
6936            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6937        );
6938        return Ok(run_async(gen));
6939    }
6940    let home = fv
6941        .home_class
6942        .as_ref()
6943        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6944    // Resolved BEFORE the borrow below: reading the function table re-enters
6945    // the host, and doing it inside the frame-push closure double-borrows.
6946    let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6947    with_host(|h| {
6948        h.frames.push(Frame {
6949            base_env: env.clone(),
6950            env,
6951            this_obj: this_val,
6952            new_target,
6953            home_class: home,
6954            home_static: fv.home_static,
6955            home_object: fv.home_object.clone(),
6956            strict: fn_strict,
6957            line: 0,
6958            owner: Some(def_name),
6959            is_module: false,
6960            this_state: if derived_ctor {
6961                ThisState::Pending
6962            } else {
6963                ThisState::Plain
6964            },
6965        })
6966    });
6967    let r = run_chunk_keyed(func_key(fv.def_id), || {
6968        with_host(|h| h.funcs[fv.def_id].chunk.clone())
6969    });
6970    let (sig, this_state) = with_host(|h| {
6971        let frame = h.frames.pop();
6972        (h.signal.take(), frame.map(|f| f.this_state))
6973    });
6974    let ret = match r {
6975        Err(e) => return Err(e),
6976        Ok(_) => match sig {
6977            Some(Signal::Return(v)) => v,
6978            _ => Value::Undef,
6979        },
6980    };
6981    // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
6982    // return wins; any other non-undefined return is a TypeError; and falling
6983    // off the end (or `return;`) needs `this` to have been bound by `super()`.
6984    if derived_ctor && !returns_object(&ret) {
6985        if !matches!(ret, Value::Undef) {
6986            return Err(type_error(
6987                "Derived constructors may only return object or undefined",
6988            ));
6989        }
6990        if this_state == Some(ThisState::Pending) {
6991            return Err(this_before_super_error());
6992        }
6993    }
6994    Ok(ret)
6995}
6996
6997/// Bind positional args into a fresh call environment. The compiler emits the
6998/// param names in `def.params`; a `...rest` slot collects the tail as an array.
6999fn bind_params(
7000    env: &Env,
7001    params: &[ParamSlot],
7002    args: Vec<Value>,
7003    is_arrow: bool,
7004    callee: Option<&Value>,
7005    sloppy: bool,
7006) {
7007    let mut vars = VarMap::default();
7008    let mut i = 0;
7009    for slot in params {
7010        if slot.rest {
7011            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7012            let arr = with_host(|h| h.new_array(rest));
7013            vars.insert(slot.name.clone(), arr);
7014        } else {
7015            let v = args.get(i).cloned().unwrap_or(Value::Undef);
7016            vars.insert(slot.name.clone(), v);
7017            i += 1;
7018        }
7019    }
7020    // `arguments` array (simple approximation — see BUGS.md: it is a real
7021    // Array, not an Arguments exotic). An ARROW function never gets one:
7022    // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7023    // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7024    // enclosing function's. Binding an empty one here made
7025    // `function f(){ const g = () => [...arguments]; }` see zero args.
7026    if !is_arrow {
7027        let args_arr = with_host(|h| {
7028            let a = h.new_array(args);
7029            // Marked so it can be told apart from an ordinary array: node's
7030            // `arguments` is an exotic, and without the mark
7031            // `Array.isArray(arguments)` was true, the brand was
7032            // `[object Array]` and `util.types.isArgumentsObject` was false.
7033            // The backing representation stays an Array, which is what keeps
7034            // indices, `length`, spread and `for-of` working.
7035            h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7036            // `callee` is the function itself in SLOPPY code (it is a poison
7037            // pill only in strict, which the read path handles). It read back
7038            // `undefined`, so the pre-`class` self-reference idiom
7039            // `(function(){ arguments.callee })` found nothing.
7040            if let Some(f) = callee {
7041                if sloppy {
7042                    h.set_fn_prop(&a, "@@callee", f.clone());
7043                }
7044            }
7045            a
7046        });
7047        vars.entry("arguments".to_string()).or_insert(args_arr);
7048    }
7049    env.borrow_mut().vars = vars;
7050}
7051
7052/// Construct an instance with `new` — creates a fresh object, binds it as
7053/// `this`, runs the constructor, and returns the object (unless the constructor
7054/// returns its own object).
7055pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7056    construct_nt(ctor, args, ctor.clone())
7057}
7058
7059/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7060/// calls `super(...)` — the target stays the originally-`new`ed class).
7061pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7062    // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7063    if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7064        return crate::proxy::construct(ctor, args, &new_target)
7065            .map(|r| r.expect("kind_of said Proxy"));
7066    }
7067    let obj = with_host(|h| h.get(ctor).cloned());
7068    match obj {
7069        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7070        Some(JsObj::Func(fv)) => {
7071            // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7072            // `function*` and an `async function` are callable but not
7073            // constructable (10.2.2 is installed only for the ordinary case), so
7074            // `new` on one is a TypeError — node-js instead ran the body and
7075            // handed back a half-built instance (for a generator, an object whose
7076            // constructor had returned a suspended generator).
7077            let non_ctor = with_host(|h| {
7078                h.funcs
7079                    .get(fv.def_id)
7080                    // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7081                    // is `TypeError: o.m is not a constructor` on node v26.7.0,
7082                    // which is also why a method owns no `prototype`.
7083                    .map(|d| d.is_generator || d.is_async || d.is_method)
7084                    .unwrap_or(false)
7085            });
7086            if fv.is_arrow || non_ctor {
7087                return Err(not_a_constructor(ctor));
7088            }
7089            // A plain constructor function: instance delegates to `fn.prototype`
7090            // (auto-created with a `.constructor` back-link if not yet accessed).
7091            let inst = with_host(|h| {
7092                let o = h.new_object(IndexMap::new());
7093                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7094                    let p = h.new_object(IndexMap::new());
7095                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7096                        pp.insert("constructor".to_string(), ctor.clone());
7097                    }
7098                    // `F.prototype.constructor` is non-enumerable in JS.
7099                    h.hide_prop(&p, "constructor");
7100                    h.set_fn_prop(ctor, "prototype", p.clone());
7101                    p
7102                });
7103                h.set_proto(&o, proto);
7104                o
7105            });
7106            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7107            if returns_object(&r) {
7108                Ok(r)
7109            } else {
7110                Ok(inst)
7111            }
7112        }
7113        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7114        Some(JsObj::BoundFunc {
7115            target, args: pre, ..
7116        }) => {
7117            let mut all = pre;
7118            all.extend(args);
7119            construct_nt(&target, all, new_target)
7120        }
7121        _ => Err(not_a_constructor(ctor)),
7122    }
7123}
7124
7125/// `TypeError: <callee> is not a constructor`.
7126///
7127/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7128/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7129/// its name — the same string in the common case — and anything else by its
7130/// value.
7131fn not_a_constructor(ctor: &Value) -> String {
7132    let name = with_host(|h| match h.callable_name(ctor) {
7133        n if n.is_empty() => h.str_of(ctor),
7134        n => n,
7135    });
7136    type_error(&format!("{name} is not a constructor"))
7137}
7138
7139/// Whether a constructor's return value is an object (so `new` yields it instead
7140/// of the fresh instance). In JS "object" includes functions — the `router`
7141/// package's constructor `return router` (a function) must be honored, or the
7142/// returned router loses its callable identity.
7143fn returns_object(r: &Value) -> bool {
7144    matches!(
7145        with_host(|h| h.get(r).cloned()),
7146        Some(JsObj::Object(_))
7147            | Some(JsObj::Array(_))
7148            | Some(JsObj::Map { .. })
7149            | Some(JsObj::Set { .. })
7150            | Some(JsObj::Func(_))
7151            | Some(JsObj::Class(_))
7152            | Some(JsObj::BoundFunc { .. })
7153            | Some(JsObj::BoundMethod { .. })
7154            | Some(JsObj::RegExp(_))
7155    )
7156}
7157
7158/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7159/// run field initializers + the constructor (which may call `super(...)`).
7160fn construct_class(
7161    class_val: &Value,
7162    args: Vec<Value>,
7163    new_target: Value,
7164) -> Result<Value, String> {
7165    let cv = match with_host(|h| h.get(class_val).cloned()) {
7166        Some(JsObj::Class(c)) => c,
7167        _ => return Err(type_error("not a class")),
7168    };
7169    // Resolve the prototype of the *most-derived* class being `new`ed, so an
7170    // instance created through a `super()` chain still delegates to the leaf
7171    // prototype (correct method resolution).
7172    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7173        Some(JsObj::Class(c)) => c.proto.clone(),
7174        _ => cv.proto.clone(),
7175    };
7176    let inst = with_host(|h| {
7177        let o = h.new_object(IndexMap::new());
7178        h.set_proto(&o, leaf_proto.clone());
7179        o
7180    });
7181    // A `super()` deeper in may substitute the instance; the previous value is
7182    // restored so a `new` inside a constructor body cannot be mistaken for one.
7183    let saved = with_host(|h| h.swap_super_replacement(None));
7184    let ran = run_class_ctor(&cv, &inst, args, &new_target);
7185    let substituted = with_host(|h| {
7186        let s = h.take_super_replacement();
7187        h.swap_super_replacement(saved);
7188        s
7189    });
7190    // A constructor that returns an object replaces the instance (`new`
7191    // semantics); failing that, whatever `super()` substituted for it.
7192    match ran? {
7193        Some(obj) if returns_object(&obj) => Ok(obj),
7194        _ => Ok(substituted.unwrap_or(inst)),
7195    }
7196}
7197
7198/// Run one class's field initializers then its constructor on an existing
7199/// instance. Returns the constructor's explicit object return (if any). For a
7200/// base class this is the whole init; for a derived class the constructor body
7201/// reaches `super(...)` which recurses into the parent.
7202fn run_class_ctor(
7203    cv: &ClassVal,
7204    inst: &Value,
7205    args: Vec<Value>,
7206    new_target: &Value,
7207) -> Result<Option<Value>, String> {
7208    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7209    // that. A base class initializes fields before the constructor body.
7210    if cv.parent.is_none() {
7211        init_fields(cv, inst)?;
7212    }
7213    match &cv.ctor {
7214        Some(ctor_fn) => {
7215            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7216                Some(JsObj::Func(f)) => f,
7217                _ => return Err(type_error("class constructor is not a function")),
7218            };
7219            if cv.parent.is_some() {
7220                with_host(|h| h.mark_next_call_derived_ctor());
7221            }
7222            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7223            return Ok(Some(r));
7224        }
7225        None => {
7226            // Default constructor: `constructor(...a){ super(...a); }` for a
7227            // derived class, empty for a base class.
7228            if let Some(parent) = &cv.parent {
7229                // A base constructor's returned object becomes the instance, so
7230                // the implicit `constructor(...a){ super(...a) }` hands it on.
7231                if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7232                    init_fields(cv, &replacement)?;
7233                    return Ok(Some(replacement));
7234                }
7235                init_fields(cv, inst)?;
7236            }
7237        }
7238    }
7239    Ok(None)
7240}
7241
7242/// Evaluate and assign a class's instance-field initializers on `inst`.
7243fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7244    for (name, thunk, name_anon) in &cv.fields {
7245        init_one_field(inst, name, thunk, *name_anon)?;
7246    }
7247    Ok(())
7248}
7249
7250/// Evaluate ONE instance-field initializer thunk and install the result on
7251/// `inst`.
7252///
7253/// Shared by the base-class path (`init_fields`) and the derived-class path
7254/// that runs after `super(...)`; the two used to be separate loops, and only the
7255/// first canonicalized an array-index key.
7256///
7257/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7258/// gives the function the name `f`. It is decided by the compiler from the
7259/// syntax, never from the value.
7260pub fn init_one_field(
7261    inst: &Value,
7262    name: &str,
7263    thunk: &Value,
7264    name_anon: bool,
7265) -> Result<(), String> {
7266    // The thunk is an arrow capturing the class scope; run it with `this`=inst
7267    // so `this.other`-referencing initializers work.
7268    let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7269    with_host(|h| {
7270        if name_anon {
7271            let s = h.new_str(name.to_string());
7272            h.set_fn_prop(&val, "name", s);
7273        }
7274        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7275            let is_new = !props.contains_key(name);
7276            props.insert(name.to_string(), val);
7277            if is_new && array_index(name).is_some() {
7278                canonicalize_own_keys(props);
7279            }
7280        }
7281    });
7282    Ok(())
7283}
7284
7285/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7286/// kind (class vs plain function vs builtin) using the existing instance.
7287/// Run the parent constructor against `inst`.
7288///
7289/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7290/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7291/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7292/// derived constructor writes to the object `new` will hand back.
7293pub fn super_construct(
7294    parent: &Value,
7295    args: Vec<Value>,
7296    inst: &Value,
7297    new_target: &Value,
7298) -> Result<Option<Value>, String> {
7299    match with_host(|h| h.get(parent).cloned()) {
7300        Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7301            .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7302        Some(JsObj::Func(fv)) => {
7303            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7304            Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7305        }
7306        Some(JsObj::Builtin(name)) => {
7307            let built = crate::builtins::construct_builtin(&name, args)?;
7308            // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7309            // the heap variant, not in a property map, so copying own props
7310            // cannot carry it: the instance has to BECOME the built object.
7311            // Without this `new (class extends Array {})().push` was not a
7312            // function, and the same for Map, Set, RegExp, Promise and
7313            // Function — subclassing a builtin produced a plain object.
7314            if !become_exotic(inst, &built) {
7315                // An `Error` subclass is ordinary: its state IS own properties.
7316                adopt_own_props(inst, &built);
7317            }
7318            Ok(None)
7319        }
7320        // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7321        // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7322        // to the target). node-js initializes an ALREADY-allocated `inst` rather
7323        // than adopting the constructor's return value, so what the proxy built
7324        // is moved across — the same move the builtin arm makes.
7325        Some(JsObj::Proxy { .. }) => {
7326            let built = construct_nt(parent, args, new_target.clone())?;
7327            if !become_exotic(inst, &built) {
7328                adopt_own_props(inst, &built);
7329            }
7330            Ok(None)
7331        }
7332        _ => Err(type_error("super is not a constructor")),
7333    }
7334}
7335
7336/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7337/// a parent constructor produces a fresh object but node-js's class model has
7338/// already allocated the instance `this` is bound to.
7339/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7340/// extends a builtin EXOTIC really is one.
7341///
7342/// `inst` keeps its identity and its prototype link — the leaf class's
7343/// prototype, which is what method resolution and `instanceof` walk — while its
7344/// contents become the exotic the parent constructor produced. The side tables
7345/// keyed by heap index (array holes, property attributes, the fn-prop table)
7346/// move across with it.
7347///
7348/// Returns false for a variant whose state is ordinary own properties
7349/// (`Error`), which the caller copies instead.
7350fn become_exotic(inst: &Value, built: &Value) -> bool {
7351    let exotic = matches!(
7352        with_host(|h| h.get(built).cloned()),
7353        Some(JsObj::Array(_))
7354            | Some(JsObj::Map { .. })
7355            | Some(JsObj::Set { .. })
7356            | Some(JsObj::RegExp(_))
7357            | Some(JsObj::Promise { .. })
7358            | Some(JsObj::Func(_))
7359            | Some(JsObj::Str(_))
7360            | Some(JsObj::BigInt(_))
7361            | Some(JsObj::Symbol { .. })
7362    );
7363    if !exotic {
7364        return false;
7365    }
7366    let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7367        return false;
7368    };
7369    let (dst, src) = (*dst, *src);
7370    with_host(|h| {
7371        if let Some(obj) = h.get(built).cloned() {
7372            if let Some(slot) = h.get_mut(inst) {
7373                *slot = obj;
7374            }
7375        }
7376        h.move_index_state(src, dst);
7377    });
7378    true
7379}
7380
7381fn adopt_own_props(inst: &Value, built: &Value) {
7382    let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7383        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7384        _ => Vec::new(),
7385    });
7386    with_host(|h| {
7387        let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7388        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7389            for (k, v) in entries {
7390                props.insert(k, v);
7391            }
7392            canonicalize_own_keys(props);
7393        }
7394        // The copied slots keep the attributes the source gave them, so
7395        // `class E extends Error` instances hide `message`/`stack` too.
7396        for k in keys {
7397            let a = h.prop_attrs(built, &k);
7398            h.set_prop_attrs(inst, &k, a);
7399        }
7400    });
7401}
7402
7403// ── class construction (runtime) ─────────────────────────────────────────────
7404
7405/// Build a class constructor value from its parts. The compiler emits (via
7406/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7407/// undefined for a default constructor); methods/getters/setters/statics/fields
7408/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7409pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7410    // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7411    // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7412    // trap and so re-enters the host, which the borrow below cannot allow.
7413    // Without it the link fell back to `Object.prototype` and every inherited
7414    // method went missing.
7415    let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7416        .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7417        .flatten();
7418    with_host(|h| {
7419        let parent_opt = if matches!(parent, Value::Undef) {
7420            None
7421        } else {
7422            Some(parent.clone())
7423        };
7424        // The class prototype delegates to the parent's prototype (or
7425        // Object.prototype for a base class). Extending a builtin error links to
7426        // that error's prototype so `instanceof Error` holds for the subclass.
7427        let parent_proto = match &parent_opt {
7428            Some(_) if proxy_parent_proto.is_some() => {
7429                proxy_parent_proto.clone().expect("checked is_some")
7430            }
7431            Some(p) => match h.get(p).cloned() {
7432                Some(JsObj::Class(pc)) => pc.proto.clone(),
7433                Some(JsObj::Builtin(bn)) => {
7434                    h.ensure_error_protos();
7435                    h.ensure_native_protos();
7436                    // `class S extends String {}` links to the REAL
7437                    // `String.prototype`, the same way an error subclass links
7438                    // to its error prototype. Without it `S.prototype`'s
7439                    // `[[Prototype]]` fell back to `Object.prototype`, so
7440                    // `new S("hi") instanceof String` read false and
7441                    // `String(new S("hi"))` reported `[object String]` instead
7442                    // of `hi`.
7443                    error_proto_of(h, &bn)
7444                        .or_else(|| h.native_proto(&bn))
7445                        .or_else(|| h.fn_prop(p, "prototype"))
7446                        .unwrap_or_else(|| h.object_proto())
7447                }
7448                _ => h
7449                    .fn_prop(p, "prototype")
7450                    .unwrap_or_else(|| h.object_proto()),
7451            },
7452            None => h.object_proto(),
7453        };
7454        let proto = h.new_object(IndexMap::new());
7455        h.set_proto(&proto, parent_proto);
7456        let ctor_opt = if matches!(ctor, Value::Undef) {
7457            None
7458        } else {
7459            Some(ctor.clone())
7460        };
7461        // Give the constructor closure its home class (for `super.method()`), and
7462        // record its `.name`.
7463        if let Some(cf) = &ctor_opt {
7464            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7465                f.home_class = Some(name.to_string());
7466            }
7467        }
7468        let cval = ClassVal {
7469            name: name.to_string(),
7470            ctor: ctor_opt,
7471            parent: parent_opt,
7472            proto: proto.clone(),
7473            statics: IndexMap::new(),
7474            fields: Vec::new(),
7475            source_def,
7476        };
7477        let class_val = h.alloc(JsObj::Class(cval));
7478        h.class_registry.insert(name.to_string(), class_val.clone());
7479        // Link prototype → class (for instance display + `constructor`), and give
7480        // the class its own `prototype` fn-prop so `C.prototype` reads work.
7481        h.tag_proto_class(&proto, class_val.clone());
7482        h.set_fn_prop(&class_val, "prototype", proto.clone());
7483        // `Class.prototype.constructor === Class`.
7484        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7485            p.insert("constructor".to_string(), class_val.clone());
7486        }
7487        h.hide_prop(&proto, "constructor");
7488        class_val
7489    })
7490}
7491
7492/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7493/// `member::*` tag; `is_static` targets the constructor side.
7494pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7495    with_host(|h| {
7496        let cname = match h.get(class_val) {
7497            Some(JsObj::Class(c)) => c.name.clone(),
7498            _ => String::new(),
7499        };
7500        // A private method/accessor: remember which class declared it, so a
7501        // brand-check failure can name the class the way node does. A static
7502        // FIELD is data, not a method, so it keeps the field wording.
7503        if name.starts_with('#') && kind != member::STATIC_FIELD {
7504            h.note_private_method(name);
7505        }
7506        // Give the method its home class for `super.x()`, and record whether it
7507        // is static — `super` resolves against a different object either way.
7508        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7509            f.home_class = Some(cname);
7510            f.home_static = is_static;
7511        }
7512        // Static members live on the constructor (fn-props / static accessors);
7513        // instance members on the prototype.
7514        let target = if is_static {
7515            class_val.clone()
7516        } else {
7517            match h.get(class_val) {
7518                Some(JsObj::Class(c)) => c.proto.clone(),
7519                _ => return,
7520            }
7521        };
7522        match kind {
7523            member::GET => h.set_accessor(&target, name, Some(func), None),
7524            member::SET => h.set_accessor(&target, name, None, Some(func)),
7525            _ => {
7526                // A static field is enumerable (`Object.keys(C)` lists it) unlike
7527                // a method, so it must not reach the `hide_prop` below.
7528                if kind == member::STATIC_FIELD {
7529                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7530                        c.statics.insert(name.to_string(), func.clone());
7531                    }
7532                    h.set_fn_prop(class_val, name, func);
7533                    return;
7534                }
7535                if is_static {
7536                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7537                        c.statics.insert(name.to_string(), func.clone());
7538                    }
7539                    h.set_fn_prop(class_val, name, func);
7540                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7541                    p.insert(name.to_string(), func);
7542                }
7543            }
7544        }
7545        // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7546        // Evaluation), so `for (k in instance)` walking the prototype chain never
7547        // yields them and `Object.keys(C.prototype)` is empty.
7548        h.hide_prop(&target, name);
7549    });
7550}
7551
7552/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7553pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7554    with_host(|h| {
7555        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7556            c.fields.push((name.to_string(), thunk, name_anon));
7557        }
7558    });
7559}
7560
7561/// The `[[Prototype]]` object a constructor value hands to its instances
7562/// (`Ctor.prototype`), for `instanceof`.
7563fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7564    match h.get(ctor) {
7565        Some(JsObj::Class(c)) => Some(c.proto.clone()),
7566        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7567        // A builtin's prototype lives in one of two registries: the error
7568        // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7569        // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7570        // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7571        // `Buffer.prototype instanceof Uint8Array` read false even though the
7572        // link was there — the instance case only passed via a native-tag
7573        // special case, which a prototype object does not carry.
7574        Some(JsObj::Builtin(name)) => h
7575            .error_protos
7576            .get(name)
7577            .or_else(|| h.native_protos.get(name))
7578            .cloned(),
7579        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7580        _ => None,
7581    }
7582}
7583
7584/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7585/// so a chain walk driven by that function can compare the two with `strict_eq`.
7586///
7587/// `ctor_prototype` answers only for the constructors whose prototype object
7588/// really exists on the heap (classes, user functions, the error and native
7589/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7590/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7591/// mirrors that fallback rather than reporting "no prototype" and failing every
7592/// comparison.
7593fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7594    if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7595        return Some(p);
7596    }
7597    let name = with_host(|h| match h.get(ctor) {
7598        Some(JsObj::Builtin(n)) => Some(n.clone()),
7599        _ => None,
7600    })?;
7601    if name == "Object" {
7602        return Some(with_host(|h| h.object_proto()));
7603    }
7604    Some(with_host(|h| {
7605        h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7606    }))
7607}
7608
7609/// V8's "not a function" wording for a value that was expected to be callable.
7610/// A number/string/boolean is named WITH its value (`number 1 is not a
7611/// function`, `string "s" is not a function`); every other type is named by type
7612/// alone (`object is not a function`, `symbol is not a function`).
7613pub fn not_a_function_message(v: &Value) -> String {
7614    with_host(|h| match v {
7615        Value::Undef => "undefined is not a function".into(),
7616        Value::Bool(b) => format!("boolean {b} is not a function"),
7617        Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7618        Value::Str(s) => format!("string \"{s}\" is not a function"),
7619        Value::Obj(_) => match h.get(v) {
7620            Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7621            Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7622            Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7623            _ => "object is not a function".into(),
7624        },
7625        _ => "object is not a function".into(),
7626    })
7627}
7628
7629/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7630/// `ctor.prototype`.
7631pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7632    // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7633    // right-hand side REPLACES the prototype-chain walk entirely, and it is
7634    // consulted before the callability check — which is why a plain (uncallable)
7635    // object that defines it is a legal `instanceof` right-hand side.
7636    if matches!(ctor, Value::Obj(_)) {
7637        // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7638        // plain function both land in the fn-prop side table (which
7639        // `class_static` reads, following the `extends` chain), NOT in an object
7640        // property map — so consulting only `lookup_chain` would find the object
7641        // literal form and silently miss the two forms V8 users actually write.
7642        let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7643            Some(f) => Some(f),
7644            None => protocol_lookup(ctor, "@@hasInstance")?,
7645        };
7646        // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7647        // else that is not callable is a TypeError, so a data property here does
7648        // NOT fall back to the prototype walk.
7649        match handler {
7650            Some(f) if with_host(|h| is_callable(h, &f)) => {
7651                let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7652                return Ok(with_host(|h| h.truthy(&r)));
7653            }
7654            Some(f)
7655                if !matches!(f, Value::Undef)
7656                    && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7657            {
7658                return Err(type_error(&not_a_function_message(&f)));
7659            }
7660            _ => {}
7661        }
7662    }
7663    // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7664    // `1 instanceof 3` throws even though the left side could never match.
7665    // Returning early on the left side skipped that check entirely.
7666    let ctor_callable = with_host(|h| {
7667        matches!(
7668            h.get(ctor),
7669            Some(JsObj::Func(_))
7670                | Some(JsObj::Class(_))
7671                | Some(JsObj::Builtin(_))
7672                | Some(JsObj::BoundFunc { .. })
7673        )
7674    });
7675    if !ctor_callable {
7676        // V8 has TWO messages here and they are not interchangeable: a primitive
7677        // right-hand side is "not an object", an object that is merely not
7678        // callable is "not callable". Only the second was implemented, so
7679        // `1 instanceof 3` reported nothing at all.
7680        return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7681            "Right-hand side of 'instanceof' is not callable"
7682        } else {
7683            "Right-hand side of 'instanceof' is not an object"
7684        }));
7685    }
7686    // A non-object left-hand side is never an instance — but only after the
7687    // right-hand side has been validated above.
7688    if !matches!(obj, Value::Obj(_)) {
7689        return Ok(false);
7690    }
7691    // A Proxy shares no heap variant with its target, so the structural arms
7692    // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7693    // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7694    // walk here, which also gives a custom trap the final say.
7695    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7696        with_host(|h| {
7697            h.ensure_error_protos();
7698            h.ensure_native_protos();
7699        });
7700        let Some(target) = walk_target_prototype(ctor) else {
7701            return Ok(false);
7702        };
7703        let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7704        for _ in 0..100 {
7705            if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7706                return Ok(false);
7707            }
7708            if with_host(|h| h.strict_eq(&cur, &target)) {
7709                return Ok(true);
7710            }
7711            cur = crate::builtins::prototype_of(&cur);
7712        }
7713        return Ok(false);
7714    }
7715    // Builtin constructors whose instances aren't prototype-linked in our model
7716    // (arrays/plain objects/functions) get a structural instanceof.
7717    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7718        // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7719        // an instance regardless of its own kind, which is the whole of the ES5
7720        // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7721        // makes `new F() instanceof Array` true. A structural test alone said
7722        // false.
7723        if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7724            return Ok(true);
7725        }
7726        let kind = with_host(|h| h.get(obj).cloned());
7727        match name.as_str() {
7728            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7729            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7730            // Map/Set/Promise instances are distinct heap variants, not
7731            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7732            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7733            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7734            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7735            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7736            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7737            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7738            // A RegExp is its own heap variant too, not a prototype-linked object.
7739            "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7740            "Object" => {
7741                // Everything object-typed except a null-prototype object is an
7742                // Object instance.
7743                let is_obj = matches!(
7744                    kind,
7745                    Some(JsObj::Object(_))
7746                        | Some(JsObj::Array(_))
7747                        // A namespace object and a builtin function are both
7748                        // `instanceof Object`: `Math instanceof Object` is true.
7749                        | Some(JsObj::Builtin(_))
7750                        | Some(JsObj::Func(_))
7751                        | Some(JsObj::Class(_))
7752                        | Some(JsObj::Map { .. })
7753                        | Some(JsObj::Set { .. })
7754                        | Some(JsObj::Promise { .. })
7755                        | Some(JsObj::Generator { .. })
7756                        | Some(JsObj::RegExp(_))
7757                );
7758                if is_obj {
7759                    // A null-prototype object (Object.create(null) or
7760                    // setPrototypeOf(o, null)) is NOT an Object instance.
7761                    if with_host(|h| h.has_null_proto(obj)) {
7762                        return Ok(false);
7763                    }
7764                    return Ok(true);
7765                }
7766                return Ok(false);
7767            }
7768            // A Node `Buffer` IS a `Uint8Array` subclass instance.
7769            "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7770                return Ok(true);
7771            }
7772            // Every typed array carries the same `TypedArray` tag; the constructor
7773            // it is an instance of is its ELEMENT KIND.
7774            k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7775                return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7776            }
7777            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7778            // `TextEncoder`, …) is an instance of the builtin whose name matches
7779            // its hidden `@@native` tag.
7780            other => {
7781                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7782                    return Ok(true);
7783                }
7784            }
7785        }
7786    }
7787    with_host(|h| h.ensure_error_protos());
7788    // The native exotic prototypes are built lazily; `instanceof` may be the
7789    // first thing to ask for them, so materialise them before the chain walk.
7790    with_host(|h| h.ensure_native_protos());
7791    let target = match with_host(|h| ctor_prototype(h, ctor)) {
7792        Some(p) => p,
7793        None => return Ok(false),
7794    };
7795    let mut cur = with_host(|h| h.proto_of(obj));
7796    while let Some(p) = cur {
7797        if with_host(|h| h.strict_eq(&p, &target)) {
7798            return Ok(true);
7799        }
7800        cur = with_host(|h| h.proto_of(&p));
7801    }
7802    Ok(false)
7803}
7804
7805// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7806
7807impl JsHost {
7808    /// Swap the volatile execution context in one shot, returning the previous
7809    /// one — installs a generator's context on resume, pulls it back on suspend.
7810    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7811        std::mem::swap(&mut self.frames, &mut c.frames);
7812        std::mem::swap(&mut self.error, &mut c.error);
7813        std::mem::swap(&mut self.exc, &mut c.exc);
7814        std::mem::swap(&mut self.signal, &mut c.signal);
7815        c
7816    }
7817    pub fn is_generator_val(&self, v: &Value) -> bool {
7818        matches!(self.get(v), Some(JsObj::Generator { .. }))
7819    }
7820    /// Whether `v` is an ASYNC generator object — the borrow-free form of
7821    /// [`is_async_generator`], usable from code already holding the host.
7822    pub fn is_async_gen_val(&self, v: &Value) -> bool {
7823        match self.get(v) {
7824            Some(JsObj::Generator { id }) => self
7825                .generators
7826                .get(*id as usize)
7827                .map(|g| g.async_gen)
7828                .unwrap_or(false),
7829            _ => false,
7830        }
7831    }
7832    pub fn gen_done(&self, id: u32) -> bool {
7833        self.generators
7834            .get(id as usize)
7835            .map(|g| g.done)
7836            .unwrap_or(true)
7837    }
7838    fn gen_started(&self, id: u32) -> bool {
7839        self.generators
7840            .get(id as usize)
7841            .map(|g| g.started)
7842            .unwrap_or(false)
7843    }
7844}
7845
7846/// Build a suspended generator whose body is `chunk`, run in a frame with the
7847/// already-bound `env`. Nothing executes until the first `gen_resume`.
7848fn make_generator(
7849    chunk: Chunk,
7850    env: Env,
7851    this_val: Option<Value>,
7852    home_class: Option<String>,
7853    home_static: bool,
7854    home_object: Option<Value>,
7855    strict: bool,
7856) -> Value {
7857    let home = home_class
7858        .as_ref()
7859        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7860    let frame = Frame {
7861        base_env: env.clone(),
7862        env,
7863        this_obj: this_val,
7864        new_target: None,
7865        home_class: home,
7866        home_static,
7867        home_object,
7868        strict,
7869        line: 0,
7870        owner: None,
7871        is_module: false,
7872        this_state: ThisState::Plain,
7873    };
7874    let id = with_host(|h| {
7875        let id = h.generators.len() as u32;
7876        h.generators.push(GenCell {
7877            coro: None,
7878            yielder: std::ptr::null(),
7879            ctx: GenContext {
7880                frames: vec![frame],
7881                ..GenContext::default()
7882            },
7883            done: false,
7884            started: false,
7885            inject: None,
7886            async_gen: false,
7887            queue: std::collections::VecDeque::new(),
7888            running: false,
7889            stack_floor: 0,
7890        });
7891        id
7892    });
7893    let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7894        ensure_coroutine_floor();
7895        // Same thread → publish the yielder so `yield` (deep in the body's VM)
7896        // can reach it. Valid for the whole body lifetime.
7897        with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7898        let r = run_chunk_on(chunk);
7899        // A `return` inside the body leaves a Return signal carrying the final
7900        // value; capture it so `.next()` reports it as the completion value.
7901        let ret = with_host(|h| match h.signal.take() {
7902            Some(Signal::Return(v)) => v,
7903            _ => Value::Undef,
7904        });
7905        r.map(|_| ret)
7906    };
7907    // The body's stack is allocated here rather than left to `Coroutine::new` so
7908    // that its size is ours to choose and, above all, so its `limit()` is known:
7909    // that address is what `stack_exhausted` must compare against while the body
7910    // runs, since a coroutine does NOT run on the thread stack pthread reports.
7911    // A refused reservation still yields a working generator on corosensei's own
7912    // 1 MiB default, with a floor derived on entry instead.
7913    let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7914        Ok(stack) => {
7915            let floor = coro_stack_floor(&stack);
7916            (corosensei::Coroutine::with_stack(stack, body), floor)
7917        }
7918        Err(_) => (corosensei::Coroutine::new(body), 0),
7919    };
7920    with_host(|h| {
7921        h.generators[id as usize].coro = Some(coro);
7922        h.generators[id as usize].stack_floor = floor;
7923    });
7924    with_host(|h| h.alloc(JsObj::Generator { id }))
7925}
7926
7927/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7928/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7929pub fn gen_yield(v: Value) -> Result<Value, String> {
7930    let id = match CUR_GEN.with(|c| c.get()) {
7931        Some(id) => id,
7932        None => return Err(type_error("yield outside a generator")),
7933    };
7934    let yp = with_host(|h| h.generators[id as usize].yielder);
7935    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7936    // only reach here from inside that body (its stack is live).
7937    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7938    let sent = yielder.suspend(v);
7939    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7940    // convert it into a Return signal / thrown value so the body unwinds and any
7941    // `finally` runs, exactly as a source-level `return`/`throw` would.
7942    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7943        match inj {
7944            GenInject::Return(rv) => {
7945                with_host(|h| h.signal = Some(Signal::Return(rv)));
7946                return Ok(Value::Undef);
7947            }
7948            GenInject::Throw(ev) => {
7949                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7950                with_host(|h| h.exc = Some(ev));
7951                return Err(msg);
7952            }
7953        }
7954    }
7955    Ok(sent)
7956}
7957
7958/// `generator.return(v)`: force the generator to complete, running any pending
7959/// `finally`. If it is already done (or never started) it just reports
7960/// `{value:v, done:true}` without executing the body.
7961pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
7962    let id = match with_host(|h| h.get(gen).cloned()) {
7963        Some(JsObj::Generator { id }) => id,
7964        _ => return Err(type_error("not a generator")),
7965    };
7966    // Not started yet (coro present, ctx never resumed) OR already done → no body
7967    // to unwind: complete immediately with the supplied value.
7968    let started = with_host(|h| h.gen_started(id));
7969    if with_host(|h| h.generators[id as usize].done) || !started {
7970        with_host(|h| h.generators[id as usize].done = true);
7971        return Ok(GenStep::Done(v));
7972    }
7973    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
7974    gen_resume(gen, Value::Undef)
7975}
7976
7977/// `generator.throw(e)`: inject a throw at the suspension point, running any
7978/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
7979pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
7980    let id = match with_host(|h| h.get(gen).cloned()) {
7981        Some(JsObj::Generator { id }) => id,
7982        _ => return Err(type_error("not a generator")),
7983    };
7984    let started = with_host(|h| h.gen_started(id));
7985    if with_host(|h| h.generators[id as usize].done) || !started {
7986        // A throw into a done/unstarted generator propagates to the caller.
7987        with_host(|h| h.generators[id as usize].done = true);
7988        let msg = with_host(|h| crate::builtins::error_string(h, &e));
7989        with_host(|h| h.exc = Some(e));
7990        return Err(msg);
7991    }
7992    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
7993    gen_resume(gen, Value::Undef)
7994}
7995
7996/// Outcome of resuming a generator: a yielded value (not done), or the final
7997/// completion value (done).
7998pub enum GenStep {
7999    Yield(Value),
8000    Done(Value),
8001}
8002
8003/// Resume a generator until its next `yield` or its body returns. Preserves the
8004/// shared host: the coroutine is taken out so the body re-enters `with_host`
8005/// freely, and the volatile context is swapped so the caller's frames/signal
8006/// survive the switch.
8007pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8008    let id = match with_host(|h| h.get(gen).cloned()) {
8009        Some(JsObj::Generator { id }) => id,
8010        _ => return Err(type_error("not a generator")),
8011    };
8012    if with_host(|h| h.generators[id as usize].done) {
8013        return Ok(GenStep::Done(Value::Undef));
8014    }
8015    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8016        Some(c) => c,
8017        None => return Err("TypeError: generator already executing".into()),
8018    };
8019    with_host(|h| h.generators[id as usize].started = true);
8020    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8021    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8022    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8023    // The body runs on the coroutine's OWN stack, so the guard's floor has to
8024    // move with it and move back on suspend — generators nest, and a resume from
8025    // inside another generator must restore that one's floor, not the thread's.
8026    let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8027    let caller_floor = swap_stack_floor(coro_floor);
8028
8029    let out = coro.resume(send); // no host borrow held; body drives its own VM
8030
8031    let measured = swap_stack_floor(caller_floor);
8032    // A coroutine on corosensei's default stack has no known bounds, so the
8033    // floor it measured for itself on first entry is kept for later resumes.
8034    if coro_floor == 0 && measured != 0 {
8035        with_host(|h| h.generators[id as usize].stack_floor = measured);
8036    }
8037    CUR_GEN.with(|c| c.set(prev));
8038    let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8039    // A `throw` inside the body left the thrown VALUE in the generator's context,
8040    // which the swap above just stashed away. Hand it to the caller so the
8041    // rejection/catch keeps the original error object instead of a string rebuild.
8042    let thrown = gen_ctx.exc.take();
8043    with_host(|h| {
8044        if let Some(v) = thrown {
8045            h.exc = Some(v);
8046        }
8047        h.generators[id as usize].ctx = gen_ctx;
8048        h.generators[id as usize].coro = Some(coro);
8049    });
8050
8051    match out {
8052        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8053        corosensei::CoroutineResult::Return(r) => {
8054            // Release the coroutine — and with it the mmap'd stack it owns —
8055            // the moment the body completes. `h.generators` only ever grows (an
8056            // id is never reused), so a program that awaits in a loop otherwise
8057            // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8058            // the life of the process. A finished generator is never resumed:
8059            // `gen_resume` returns `Done` on the `done` flag before it looks.
8060            with_host(|h| {
8061                let g = &mut h.generators[id as usize];
8062                g.done = true;
8063                g.coro = None;
8064            });
8065            match r {
8066                Ok(v) => Ok(GenStep::Done(v)),
8067                Err(e) => Err(e),
8068            }
8069        }
8070    }
8071}
8072
8073/// Force a generator to completion (used by `.return()` and abandoned loops):
8074/// marks it done without running further.
8075pub fn gen_close(gen: &Value) {
8076    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8077        with_host(|h| h.generators[id as usize].done = true);
8078    }
8079}
8080
8081// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8082
8083/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8084pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8085    match v {
8086        Value::Undef => MapKey::Undef,
8087        Value::Bool(b) => MapKey::Bool(*b),
8088        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8089        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8090        Value::Str(s) => MapKey::Str((**s).clone()),
8091        Value::Obj(i) => match h.get(v) {
8092            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8093            Some(JsObj::Null) => MapKey::Null,
8094            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8095            Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8096            _ => MapKey::Ref(*i),
8097        },
8098        _ => MapKey::Undef,
8099    }
8100}
8101
8102/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8103fn norm_num_bits(f: f64) -> u64 {
8104    if f.is_nan() {
8105        return f64::NAN.to_bits();
8106    }
8107    if f == 0.0 {
8108        return 0.0f64.to_bits(); // fold -0 into +0
8109    }
8110    f.to_bits()
8111}
8112
8113/// Fully materialize any iterable into a vector of values.
8114/// Pull at most `n` values, then close the iterator — 8.6.2
8115/// IteratorBindingInitialization, which is what an array destructuring pattern
8116/// without a `...rest` element performs.
8117///
8118/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8119/// fixed number of targets, so the spec pulls exactly that many and calls
8120/// IteratorClose on whatever is left; draining instead made
8121///
8122/// ```text
8123/// const [first] = infiniteGenerator();
8124/// ```
8125///
8126/// run forever. It is also observable on any finite iterator, as the count of
8127/// `next()` calls and whether `return()` ever ran.
8128///
8129/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8130/// using `iter_all` and an unbounded source hangs there in node too.
8131pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8132    // A Proxy iterates through its traps, which materialize eagerly; there is
8133    // no step-wise form to bound, so this keeps the draining behaviour.
8134    if let Some(items) = crate::proxy::iterate(v)? {
8135        return Ok(items.into_iter().take(n).collect());
8136    }
8137    if with_host(|h| h.is_generator_val(v)) {
8138        let mut out = Vec::new();
8139        while out.len() < n {
8140            match gen_resume(v, Value::Undef)? {
8141                GenStep::Yield(x) => out.push(x),
8142                _ => return Ok(out), // ran out on its own; nothing left to close
8143            }
8144        }
8145        // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8146        let _ = gen_return(v, Value::Undef);
8147        return Ok(out);
8148    }
8149    if let Some(iter_fn) = user_iterator_fn(v) {
8150        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8151        let mut out = Vec::new();
8152        while out.len() < n {
8153            let step = call_method(&iterator, "next", Vec::new())?;
8154            // Read first: resolving the property re-enters the host, so doing
8155            // it inside the `with_host` closure double-borrows and aborts.
8156            let done = get_prop_chain(&step, "done")?;
8157            if with_host(|h| h.truthy(&done)) {
8158                return Ok(out);
8159            }
8160            out.push(get_prop_chain(&step, "value")?);
8161        }
8162        // IteratorClose: `return` is optional on the protocol, and a throw from
8163        // it is swallowed here the way a normal (non-abrupt) completion does.
8164        if let Ok(ret) = get_prop_chain(&iterator, "return") {
8165            if with_host(|h| is_callable(h, &ret)) {
8166                let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8167            }
8168        }
8169        return Ok(out);
8170    }
8171    // Arrays, strings, Map/Set: already materialized, and their built-in
8172    // iterators carry no `return`, so there is nothing to close. The same
8173    // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8174    // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8175    // longer carried `Symbol.iterator`.
8176    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8177        let shown = with_host(|h| h.inspect(v));
8178        return Err(type_error(&format!("{shown} is not iterable")));
8179    }
8180    with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8181}
8182
8183pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8184    // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8185    // no heap variant `iter_vec` could recognise.
8186    if let Some(items) = crate::proxy::iterate(v)? {
8187        return Ok(items);
8188    }
8189    // Generators / user iterators must resume without a live host borrow.
8190    if with_host(|h| h.is_generator_val(v)) {
8191        let mut out = Vec::new();
8192        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8193            out.push(x);
8194        }
8195        return Ok(out);
8196    }
8197    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8198    // Checked BEFORE the reachability guard below, since an own `Symbol
8199    // .iterator` makes a value iterable no matter what its prototype is.
8200    if let Some(iter_fn) = user_iterator_fn(v) {
8201        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8202        return drain_iterator(&iterator);
8203    }
8204    // The fast paths below read a builtin's backing storage directly, which is
8205    // only legitimate while that builtin's `Symbol.iterator` is still
8206    // reachable: replacing the prototype takes it away, and node then reports
8207    // the value as not iterable. Spread, destructuring and `Array.from`'s
8208    // iterable branch all funnel through here.
8209    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8210        let shown = with_host(|h| h.inspect(v));
8211        return Err(type_error(&format!("{shown} is not iterable")));
8212    }
8213    // A String wrapper iterates its code POINTS, exactly as the primitive does
8214    // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8215    if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8216        if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8217            return iter_all(&prim);
8218        }
8219    }
8220    // An array's index ACCESSORS are not in its backing vector, so iterating one
8221    // (spread, `for-of`, `Array.from`) has to resolve them the way the
8222    // `Array.prototype` methods do.
8223    let mut items = with_host(|h| h.iter_vec(v))?;
8224    if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8225        crate::builtins::resolve_index_accessors_pub(v, &mut items);
8226    }
8227    Ok(items)
8228}
8229
8230// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8231
8232/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8233/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8234/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8235/// are awaited one at a time by `async_step`.
8236pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8237    if let Some(f) = user_async_iterator_fn(src) {
8238        return invoke(&f, Vec::new(), Some(src.clone()));
8239    }
8240    // An `async function*` object IS its own async iterator; draining it into a
8241    // list here would run the whole body (and any `finally`) before the consumer
8242    // sees the first value.
8243    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8244        if with_host(|h| h.generators[id as usize].async_gen) {
8245            return Ok(src.clone());
8246        }
8247    }
8248    let items = iter_all(src)?;
8249    Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8250}
8251
8252/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8253fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8254    // A PROXY supplies the protocol through its `get` trap and is not a plain
8255    // object, so the shape test below rejects it outright.
8256    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8257        return protocol_lookup(v, "@@asyncIterator")
8258            .ok()
8259            .flatten()
8260            .filter(|f| with_host(|h| is_callable(h, f)));
8261    }
8262    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8263    if !is_plain {
8264        return None;
8265    }
8266    // Full property resolution, not a stored-property lookup — the same reason
8267    // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8268    // dispatches its methods through the stdlib method table rather than a
8269    // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8270    // even though reading it gives a function: `for await (const v of
8271    // timersPromises.setInterval(…))` said the iterator "is not iterable".
8272    let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8273    with_host(|h| is_callable(h, &f)).then_some(f)
8274}
8275
8276/// One step of a `for await` loop: return a Promise that settles to a
8277/// `{value, done}` record. For a native async iterator this is `iter.next()`
8278/// (already a promise of the record). For the sync fallback it pops the next raw
8279/// value, awaits it, and packages `{value: resolved, done:false}` (or
8280/// `{done:true}` at exhaustion).
8281pub fn async_step(iterator: &Value) -> Result<Value, String> {
8282    // An `async function*` object: resume it through the await-aware driver.
8283    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8284        if with_host(|h| h.generators[id as usize].async_gen) {
8285            return Ok(async_gen_step(iterator, Value::Undef));
8286        }
8287    }
8288    // Sync-fallback iterator: drive it here, awaiting each yielded value.
8289    if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
8290        if idx >= items.len() {
8291            // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8292            // promise even at exhaustion, so the `done: true` step costs the same
8293            // two microtask ticks a value step does.
8294            let step = with_host(|h| h.new_promise());
8295            let sid = with_host(|h| h.promise_id(&step).unwrap());
8296            with_host(|h| {
8297                h.queue_micro_native(Box::new(move || {
8298                    resolve_promise_val(sid, iter_record(Value::Undef, true));
8299                    Ok(())
8300                }))
8301            });
8302            return Ok(step);
8303        }
8304        let raw = items[idx].clone();
8305        with_host(|h| {
8306            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8307                *idx += 1;
8308            }
8309        });
8310        // Await the raw value (adopts a promise's resolution), then wrap.
8311        let step = with_host(|h| h.new_promise());
8312        let sid = with_host(|h| h.promise_id(&step).unwrap());
8313        let raw_p = promise_of(&raw);
8314        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8315        subscribe_native(
8316            raw_id,
8317            Box::new(move |state, val| {
8318                if state == PromiseState::Rejected {
8319                    reject_promise_val(sid, val);
8320                } else {
8321                    resolve_promise_val(sid, iter_record(val, false));
8322                }
8323                Ok(())
8324            }),
8325        );
8326        return Ok(step);
8327    }
8328    // Native async iterator: `iter.next()` returns the {value,done} promise.
8329    let r = call_method(iterator, "next", Vec::new())?;
8330    Ok(promise_of(&r))
8331}
8332
8333/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8334/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8335pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8336    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8337    if !is_plain {
8338        return None;
8339    }
8340    // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8341    // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8342    // stdlib method table rather than a property map, so `lookup_chain` reported
8343    // no `Symbol.iterator` for one even though reading it gave a function —
8344    // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8345    let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8346    with_host(|h| is_callable(h, &f)).then_some(f)
8347}
8348
8349/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8350/// exhaustion.
8351/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8352/// `f` exits abruptly (7.4.9 IteratorClose).
8353///
8354/// The difference from `iter_all` + a loop is that this never materializes the
8355/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8356/// stop at the first call, and draining first means it never gets there at all.
8357pub fn iter_for_each(
8358    src: &Value,
8359    mut f: impl FnMut(Value, usize) -> Result<(), String>,
8360) -> Result<(), String> {
8361    // Only a USER iterator can be infinite or observe its own close; every
8362    // other shape is already a finite materialized sequence.
8363    let Some(iter_fn) = user_iterator_fn(src) else {
8364        for (i, v) in iter_all(src)?.into_iter().enumerate() {
8365            f(v, i)?;
8366        }
8367        return Ok(());
8368    };
8369    let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8370    let mut i = 0usize;
8371    loop {
8372        let step = call_method(&iterator, "next", Vec::new())?;
8373        let done = get_prop_chain(&step, "done")?;
8374        if with_host(|h| h.truthy(&done)) {
8375            return Ok(());
8376        }
8377        let value = get_prop_chain(&step, "value")?;
8378        if let Err(e) = f(value, i) {
8379            // The callback's error wins over anything `return()` raises, so a
8380            // throwing `return` is swallowed here (7.4.9 step 6).
8381            let _ = close_iterator(&iterator);
8382            return Err(e);
8383        }
8384        i += 1;
8385    }
8386}
8387
8388/// Call `iterator.return()` if it has one, as IteratorClose does.
8389pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8390    let has = crate::builtins::get_property(iterator, "return")?;
8391    if with_host(|h| is_callable(h, &has)) {
8392        call_method(iterator, "return", Vec::new())?;
8393    }
8394    Ok(())
8395}
8396
8397pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8398    let mut out = Vec::new();
8399    loop {
8400        let step = call_method(iterator, "next", Vec::new())?;
8401        let done = get_prop_chain(&step, "done")?;
8402        if with_host(|h| h.truthy(&done)) {
8403            break;
8404        }
8405        out.push(get_prop_chain(&step, "value")?);
8406    }
8407    Ok(out)
8408}
8409
8410/// Property read that walks the prototype chain (used by iteration helpers).
8411/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8412/// `Math.floor`); the exceptions are the namespace objects a script can only
8413/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8414/// report `typeof === "object"` and carry no `name`/`length`.
8415pub fn builtin_is_callable(n: &str) -> bool {
8416    // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8417    // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8418    // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8419    // `true` to `instanceof Function`. `Function.prototype` is the one that
8420    // really IS callable (10.2.4: it is an anonymous built-in that returns
8421    // undefined), which is why it is not stripped here.
8422    if n != "Function.prototype" && n.ends_with(".prototype") {
8423        return false;
8424    }
8425    // A `match` rather than a slice scan: this runs on every callability test,
8426    // which is every call and every `ToPrimitive`, and a `contains` over the
8427    // list below compares against all 56 entries before answering "callable" —
8428    // the common case. The compiler turns the arms into a length-then-bytes
8429    // decision tree instead.
8430    !matches!(
8431        n,
8432        "Math"
8433            | "JSON"
8434            | "console"
8435            | "Reflect"
8436            | "process"
8437            | "Atomics"
8438            | "performance"
8439            | "fs"
8440            | "path"
8441            | "os"
8442            | "util"
8443            | "crypto"
8444            | "webcrypto"
8445            | "SubtleCrypto"
8446            | "querystring"
8447            | "events"
8448            | "timers"
8449            | "perf_hooks"
8450            | "async_hooks"
8451            | "diagnostics_channel"
8452            | "v8"
8453            | "dns"
8454            | "punycode"
8455            | "child_process"
8456            | "tty"
8457            | "url"
8458            | "zlib"
8459            | "string_decoder"
8460            | "http"
8461            | "net"
8462            | "buffer"
8463            | "function"
8464            | "path/win32"
8465            | "fs/promises"
8466            | "stream/promises"
8467            | "stream/consumers"
8468            | "stream/web"
8469            | "timers/promises"
8470            | "dns/promises"
8471            | "https"
8472            | "http2"
8473            | "tls"
8474            | "dgram"
8475            | "cluster"
8476            | "worker_threads"
8477            | "readline"
8478            | "readline/promises"
8479            | "repl"
8480            | "vm"
8481            | "domain"
8482            | "trace_events"
8483            | "wasi"
8484            | "inspector"
8485            | "object"
8486    )
8487        // The live `require.cache` view is a plain object to a script, not
8488        // something it can call.
8489        && n != crate::builtins::REQUIRE_CACHE
8490}
8491
8492pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8493    crate::builtins::get_property(recv, name)
8494}
8495
8496/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8497/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8498/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8499/// native-tagged instances) is an object and must be converted.
8500pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8501    match v {
8502        Value::Obj(_) => matches!(
8503            h.get(v),
8504            None | Some(JsObj::Null)
8505                | Some(JsObj::Str(_))
8506                | Some(JsObj::Symbol { .. })
8507                | Some(JsObj::BigInt(_))
8508        ),
8509        _ => true,
8510    }
8511}
8512
8513/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8514/// or `"string"`.
8515///
8516/// An object carrying a `Symbol.toPrimitive` method (internal key
8517/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8518/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8519/// the order reversed for the string hint — and takes the FIRST call whose
8520/// result is a primitive. An object that yields no primitive (a null-prototype
8521/// object has neither method) throws V8's
8522/// `TypeError: Cannot convert object to primitive value`.
8523///
8524/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8525/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8526/// to read `str_of` directly and so never invoked a user `valueOf`.
8527pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8528    if with_host(|h| is_primitive(h, v)) {
8529        return Ok(v.clone());
8530    }
8531    if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8532        if with_host(|h| is_callable(h, &f)) {
8533            let hv = with_host(|h| h.new_str(hint.to_string()));
8534            let r = invoke(&f, vec![hv], Some(v.clone()))?;
8535            if with_host(|h| is_primitive(h, &r)) {
8536                return Ok(r);
8537            }
8538            return Err(type_error("Cannot convert object to primitive value"));
8539        }
8540    }
8541    // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8542    // `"string"`, which is why `new Date() + 1` concatenates while
8543    // `new Date() - 1` is arithmetic.
8544    let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8545        "string"
8546    } else {
8547        hint
8548    };
8549    let order = if hint == "string" {
8550        ["toString", "valueOf"]
8551    } else {
8552        ["valueOf", "toString"]
8553    };
8554    // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8555    // below is for exotics whose property funnel exposes no callable
8556    // `toString`, not for an object whose own methods ran and returned
8557    // non-primitives — that case is the spec's TypeError, and branding it
8558    // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8559    // quietly produced `"[object Object]1"`.
8560    let mut called_any = false;
8561    for m in order {
8562        let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8563        if !with_host(|h| is_callable(h, &f)) {
8564            continue;
8565        }
8566        called_any = true;
8567        // On a Proxy the resolved method is a thunk bound to the TARGET, so
8568        // invoking it directly would stringify the target — `String(new
8569        // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8570        // native-code form. `call_method` re-dispatches the generic
8571        // `Function.prototype`/`Object.prototype` methods against the proxy.
8572        let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8573            call_method(v, m, Vec::new())?
8574        } else {
8575            invoke(&f, Vec::new(), Some(v.clone()))?
8576        };
8577        if with_host(|h| is_primitive(h, &r)) {
8578            return Ok(r);
8579        }
8580    }
8581    // Every object except a null-prototype one inherits `Object.prototype
8582    // .toString`, which always returns a string — so the exhausted-methods
8583    // TypeError is reachable only there. The exotics whose property funnel has
8584    // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8585    // get the same `[object Tag]` brand V8 gives them.
8586    // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8587    // for both method names, and if what came back was not callable there is
8588    // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8589    // is a TypeError on node, where branding it `[object Object]` invented a
8590    // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8591    // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8592    // the target's `[object Map]` brand exactly as the bare `Map` does.
8593    if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8594        return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8595    }
8596    Err(type_error("Cannot convert object to primitive value"))
8597}
8598
8599/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8600/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8601/// is absent or returns an object — is invoked), then rendered by `str_of`.
8602/// Returns a heap string value.
8603pub fn to_string_value(v: &Value) -> Result<Value, String> {
8604    let p = to_primitive(v, "string")?;
8605    // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8606    // refuses. `String(sym)` is the documented exception and is handled at that
8607    // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8608    // `[sym].join()`) rejects the way node does instead of silently rendering
8609    // `Symbol(desc)`.
8610    if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8611        return Err(type_error("Cannot convert a Symbol value to a string"));
8612    }
8613    Ok(with_host(|h| {
8614        let s = h.str_of(&p);
8615        h.new_str(s)
8616    }))
8617}
8618
8619/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8620/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8621pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8622    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8623        return Ok(with_host(|h| {
8624            let s = h.str_of(v);
8625            h.new_str(s)
8626        }));
8627    }
8628    to_string_value(v)
8629}
8630
8631/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8632/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8633/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8634/// under the host borrow and so can never invoke a JS `valueOf`.
8635pub fn to_number_value(v: &Value) -> Result<f64, String> {
8636    // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8637    // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8638    // and `+Symbol()` are both `TypeError` on node v26.7.0.
8639    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8640        return Err(type_error("Cannot convert a Symbol value to a number"));
8641    }
8642    if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8643        return Ok(n);
8644    }
8645    let p = to_primitive(v, "number")?;
8646    // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8647    // the argument was checked, so an object whose conversion yields a symbol or
8648    // a BigInt slipped past: `+Object(9n)` answered 9 and
8649    // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8650    // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8651    // which is why `Number(Object(9n))` is still 9.
8652    match with_host(|h| h.get(&p).cloned()) {
8653        Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8654        Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8655        _ => Ok(with_host(|h| h.to_number(&p))),
8656    }
8657}
8658
8659/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8660/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8661/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8662pub fn to_property_key(v: &Value) -> Result<String, String> {
8663    // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8664    // `o[sym]`); only an object key pays for the conversion.
8665    if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8666        return Ok(k);
8667    }
8668    let p = to_primitive(v, "string")?;
8669    Ok(with_host(|h| h.str_of(&p)))
8670}
8671
8672/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8673/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8674/// target), so `typeof` and every `is_callable` guard agree on one answer.
8675pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8676    match h.get(v) {
8677        // Not every builtin is a function: the namespace objects (`Math`,
8678        // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8679        // calling one is a `TypeError` in node exactly as `typeof` says.
8680        Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8681        Some(JsObj::Func(_))
8682        | Some(JsObj::BoundMethod { .. })
8683        | Some(JsObj::BoundFunc { .. })
8684        | Some(JsObj::Class(_)) => true,
8685        Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8686        _ => false,
8687    }
8688}
8689
8690/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8691/// stored value (methods, inherited data props). Does NOT invoke accessors.
8692/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8693/// `then` and the rest — which the spec performs with `[[Get]]`.
8694///
8695/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8696/// map and never asks the handler, so a proxy supplying a protocol method
8697/// through its `get` trap was invisible and the operation fell back to the
8698/// default. Everything else takes the cheap chain walk.
8699pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8700    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8701        let got = crate::builtins::get_property(v, key)?;
8702        return Ok((!matches!(got, Value::Undef)).then_some(got));
8703    }
8704    Ok(with_host(|h| lookup_chain(h, v, key)))
8705}
8706
8707pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8708    if let Some(JsObj::Object(p)) = h.get(recv) {
8709        if let Some(v) = p.get(key) {
8710            return Some(v.clone());
8711        }
8712    }
8713    let mut cur = h.proto_of(recv);
8714    while let Some(p) = cur {
8715        // A chain link may be a plain object OR a function/class (the `router`
8716        // package sets `Router.prototype = function(){}` and hangs its methods off
8717        // that function, so the methods live in the fn-prop side table).
8718        match h.get(&p) {
8719            Some(JsObj::Object(props)) => {
8720                if let Some(v) = props.get(key) {
8721                    return Some(v.clone());
8722                }
8723            }
8724            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8725                if let Some(v) = h.fn_prop(&p, key) {
8726                    return Some(v);
8727                }
8728            }
8729            _ => {}
8730        }
8731        cur = h.proto_of(&p);
8732    }
8733    None
8734}
8735
8736/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8737pub fn lookup_accessor(
8738    h: &JsHost,
8739    recv: &Value,
8740    key: &str,
8741) -> Option<(Option<Value>, Option<Value>)> {
8742    if let Some(a) = h.own_accessor(recv, key) {
8743        return Some(a);
8744    }
8745    let mut cur = h.proto_of(recv);
8746    while let Some(p) = cur {
8747        if let Some(a) = h.own_accessor(&p, key) {
8748            return Some(a);
8749        }
8750        cur = h.proto_of(&p);
8751    }
8752    // A STATIC accessor declared by an ancestor class. A subclass reaches its
8753    // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8754    // above reads — classes are not linked there, so that walk ended at once.
8755    // Static methods and fields already inherited because `class_static` does
8756    // this same parent walk for `fn_prop`; only accessors had no equivalent:
8757    //
8758    //     class Base { static get kind() { return 'base' } }
8759    //     class Sub extends Base {}
8760    //     Sub.plain()  // worked, a fn_prop
8761    //     Sub.kind     // undefined; node reads 'base'
8762    //
8763    // The caller invokes the getter with the class it was READ off as `this`,
8764    // so a getter reading `this.x` sees the subclass, per 10.2.4.
8765    let mut cls = recv.clone();
8766    while let Some(JsObj::Class(c)) = h.get(&cls) {
8767        let Some(parent) = c.parent.clone() else {
8768            break;
8769        };
8770        if let Some(a) = h.own_accessor(&parent, key) {
8771            return Some(a);
8772        }
8773        cls = parent;
8774    }
8775    None
8776}
8777
8778/// Register a builtin error prototype (for `instanceof Error` etc.).
8779pub fn set_error_proto(name: &str, proto: Value) {
8780    with_host(|h| {
8781        h.error_protos.insert(name.to_string(), proto);
8782    });
8783}
8784pub fn error_proto(name: &str) -> Option<Value> {
8785    with_host(|h| h.error_protos.get(name).cloned())
8786}
8787/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8788pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8789    h.error_protos.get(name).cloned()
8790}
8791
8792impl JsHost {
8793    /// `Error.prototype.toString` for an object whose prototype chain reaches
8794    /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8795    /// `None` for anything that is not an error, so the caller keeps its own
8796    /// stringification.
8797    pub fn error_to_string(&self, v: &Value) -> Option<String> {
8798        let base = self.error_protos.get("Error")?;
8799        let mut cur = self.proto_of(v);
8800        let mut is_error = false;
8801        while let Some(p) = cur {
8802            if self.strict_eq(&p, base) {
8803                is_error = true;
8804                break;
8805            }
8806            cur = self.proto_of(&p);
8807        }
8808        if !is_error {
8809            return None;
8810        }
8811        let name = lookup_chain(self, v, "name")
8812            .map(|n| self.str_of(&n))
8813            .unwrap_or_else(|| "Error".into());
8814        let message = lookup_chain(self, v, "message")
8815            .map(|m| self.str_of(&m))
8816            .unwrap_or_default();
8817        // Node's internal coded errors override `toString` as
8818        // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8819        // `@@nodeError` tag marks the errors `synth_error` built from a
8820        // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8821        // property still stringifies plainly.
8822        if let Some(JsObj::Object(p)) = self.get(v) {
8823            if p.contains_key("@@nodeError") {
8824                if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8825                    return Some(format!("{name} [{code}]: {message}"));
8826                }
8827            }
8828        }
8829        Some(match (name.is_empty(), message.is_empty()) {
8830            (true, _) => message,
8831            (false, true) => name,
8832            (false, false) => format!("{name}: {message}"),
8833        })
8834    }
8835}
8836
8837/// The set of builtin error constructor names forming the error hierarchy.
8838pub const ERROR_NAMES: &[&str] = &[
8839    "Error",
8840    "TypeError",
8841    "RangeError",
8842    "SyntaxError",
8843    "ReferenceError",
8844    "EvalError",
8845    "URIError",
8846    "AggregateError",
8847    // `assert`'s error class. It is NOT a global (node exposes it only as
8848    // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8849    // has to be a name `synth_error` recognizes: without it the head
8850    // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8851    // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8852    // was `Error` and `e.message` carried a prefix node keeps out of it.
8853    "AssertionError",
8854    // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8855    // `name` comes from the SECOND constructor argument rather than from the
8856    // class, so its prototype keeps the base default and each instance stamps
8857    // its own name into an internal slot.
8858    "DOMException",
8859];
8860
8861impl JsHost {
8862    /// Lazily build the builtin error prototype chain: `Error.prototype →
8863    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8864    /// Populated once; instances link to these so `e instanceof TypeError` and
8865    /// `e instanceof Error` both hold.
8866    /// The real `Buffer.prototype` object, building the
8867    /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8868    /// first use.
8869    ///
8870    /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8871    /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8872    /// `instanceof` had to be special-cased around it. Each prototype is a
8873    /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8874    /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8875    /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8876    pub fn ensure_native_protos(&mut self) {
8877        // The wrapper prototypes share this registry and this guard would skip
8878        // them, so they are built through their own.
8879        self.ensure_wrapper_protos();
8880        self.ensure_function_kind_protos();
8881        if self.native_protos.contains_key("Buffer") {
8882            return;
8883        }
8884        let obj_proto = self.object_proto();
8885        // `Object.prototype` is the one builtin prototype that already existed as
8886        // a real object (it is the chain root). Register it so `Object.prototype`
8887        // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8888        // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8889        // compares a real object against a thunk and reads false.
8890        self.native_protos
8891            .insert("Object".to_string(), obj_proto.clone());
8892        for m in crate::builtins::OBJECT_PROTO_METHODS {
8893            let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8894            if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8895                p.insert((*m).to_string(), thunk);
8896            }
8897            self.hide_prop(&obj_proto, m);
8898        }
8899        // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8900        // Object.prototype`, which is the chain node v26.7.0 really has. The
8901        // shared iteration methods (`every`, `map`, `filter`, …) live on the
8902        // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8903        // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8904        // Node while the intermediate owns it. `%TypedArray%` is not a global,
8905        // so it is reachable only by walking the chain — exactly as in Node.
8906        // Every element kind gets its own prototype hanging off the shared
8907        // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8908        // `Int32Array.prototype` rather than some other kind's. Linking them all
8909        // to `Uint8Array.prototype` would have been the easy version and would
8910        // have made an `Int32Array` claim the wrong prototype.
8911        let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8912        for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8913            chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8914        }
8915        // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8916        chain.push(("Buffer", Value::Undef));
8917        let mut prev: Option<Value> = None;
8918        for (ctor, parent) in chain.drain(..) {
8919            let proto = self.new_object(IndexMap::new());
8920            // Each kind hangs off the shared intermediate; `Buffer` hangs off
8921            // `Uint8Array.prototype`; the intermediate itself off
8922            // `Object.prototype`.
8923            let parent = match ctor {
8924                "TypedArray" => parent,
8925                "Buffer" => self
8926                    .native_protos
8927                    .get("Uint8Array")
8928                    .cloned()
8929                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8930                _ => self
8931                    .native_protos
8932                    .get("TypedArray")
8933                    .cloned()
8934                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8935            };
8936            self.set_proto(&proto, parent);
8937            // `%TypedArray%.prototype` has no reachable constructor global, so
8938            // it gets no `constructor` slot (Node's is the anonymous
8939            // `%TypedArray%` intrinsic).
8940            if ctor != "TypedArray" {
8941                let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8942                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8943                    p.insert("constructor".into(), ctor_val);
8944                }
8945                self.hide_prop(&proto, "constructor");
8946            }
8947            let methods: &[&str] = match ctor {
8948                "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
8949                "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
8950                // `Uint8Array` alone owns the base64/hex pair — no other view
8951                // has them, which is the whole reason they cannot live on the
8952                // shared `%TypedArray%` prototype above.
8953                "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
8954                // Every other kind's prototype owns no methods; it inherits them
8955                // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
8956                // which is per-kind and which Node really keeps there (measured:
8957                // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
8958                _ => &[],
8959            };
8960            if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
8961                let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
8962                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8963                    p.insert("BYTES_PER_ELEMENT".into(), bpe);
8964                }
8965                self.hide_prop(&proto, "BYTES_PER_ELEMENT");
8966            }
8967            for m in methods {
8968                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8969                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8970                    p.insert((*m).to_string(), thunk);
8971                }
8972                self.hide_prop(&proto, m);
8973            }
8974            self.native_protos.insert(ctor.to_string(), proto.clone());
8975            prev = Some(proto);
8976        }
8977    }
8978
8979    /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
8980    /// objects.
8981    ///
8982    /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
8983    /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
8984    /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
8985    /// String.prototype` and `w instanceof String` both read false while the
8986    /// wrapper's methods still resolved through the string funnel. Registering
8987    /// them here puts them on the same footing as `Buffer.prototype`.
8988    /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
8989    /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
8990    /// function's `[[Prototype]]` really points at.
8991    ///
8992    /// None are globals (node exposes them only through
8993    /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
8994    /// rather than among the wrapper constructors. Each hangs off
8995    /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
8996    pub fn ensure_function_kind_protos(&mut self) {
8997        if self.native_protos.contains_key("GeneratorFunction") {
8998            return;
8999        }
9000        let base = self
9001            .native_protos
9002            .get("Function")
9003            .cloned()
9004            .unwrap_or_else(|| self.object_proto());
9005        for ctor in [
9006            "GeneratorFunction",
9007            "AsyncFunction",
9008            "AsyncGeneratorFunction",
9009        ] {
9010            let proto = self.new_object(IndexMap::new());
9011            self.set_proto(&proto, base.clone());
9012            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9013            let tag = self.new_str(ctor);
9014            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9015                p.insert("constructor".into(), ctor_val);
9016                p.insert("@@toStringTag".into(), tag);
9017            }
9018            self.hide_prop(&proto, "constructor");
9019            self.hide_prop(&proto, "@@toStringTag");
9020            self.native_protos.insert(ctor.to_string(), proto);
9021        }
9022    }
9023
9024    pub fn ensure_wrapper_protos(&mut self) {
9025        if self.native_protos.contains_key("String") {
9026            return;
9027        }
9028        let obj_proto = self.object_proto();
9029        for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9030            let proto = self.new_object(IndexMap::new());
9031            self.set_proto(&proto, obj_proto.clone());
9032            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9033            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9034                p.insert("constructor".into(), ctor_val);
9035            }
9036            self.hide_prop(&proto, "constructor");
9037            // The three conversions must be here because `Object.prototype`
9038            // also defines them: without a shadowing entry a wrapper would
9039            // inherit the object forms and `String(new String("a"))` would
9040            // report `[object Object]`.
9041            //
9042            // The REST are here because the prototype is a real object a script
9043            // can read a method OFF of. Only the three were installed, on the
9044            // reasoning that `charAt`/`toFixed`/… reach the primitive through
9045            // `call_method` anyway — true for `s.charAt(0)` and false for the
9046            // generic-borrowing form: `String.prototype.trim` read `undefined`,
9047            // so `String.prototype.trim.call(s)` — and `Number.prototype
9048            // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9049            // wrapper method — threw. `Array.prototype`/`Object.prototype`
9050            // already carried their whole method set; these three did not.
9051            let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9052                .into_iter()
9053                .chain(match ctor {
9054                    "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9055                    "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9056                    _ => [].iter().copied(),
9057                })
9058                // The symbol-keyed methods come from the generated intrinsic
9059                // table, so this object advertises exactly the symbol methods
9060                // node defines on it — `String.prototype[Symbol.iterator]` was
9061                // `undefined` because only the string-keyed lists were walked.
9062                .chain(crate::builtins::proto_symbol_methods(ctor))
9063                .collect();
9064            let mut seen: Vec<&str> = Vec::new();
9065            for m in methods {
9066                if seen.contains(&m) {
9067                    continue;
9068                }
9069                seen.push(m);
9070                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9071                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9072                    p.insert(m.to_string(), thunk);
9073                }
9074                self.hide_prop(&proto, m);
9075            }
9076            // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9077            // prototypes that carry a `@@toStringTag`; the other three are
9078            // branded by their internal slot instead, and node reports
9079            // `undefined` for their tag. Without it
9080            // `Object.prototype.toString.call(Symbol.prototype)` read
9081            // `[object Object]` where node says `[object Symbol]`.
9082            if matches!(ctor, "Symbol" | "BigInt") {
9083                let tag = self.new_str(ctor.to_string());
9084                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9085                    p.insert("@@toStringTag".into(), tag);
9086                }
9087                // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9088                // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9089                // writable.
9090                self.set_prop_attrs(
9091                    &proto,
9092                    "@@toStringTag",
9093                    PropAttrs {
9094                        writable: false,
9095                        enumerable: false,
9096                        configurable: true,
9097                    },
9098                );
9099            }
9100            // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9101            // numeric conversion of a symbol reaches FIRST. Its absence was
9102            // observable in the failure wording: `String(Symbol.prototype)`
9103            // throws in node because `@@toPrimitive` rejects a non-Symbol
9104            // `this`, and here the conversion fell through to `toString` and
9105            // named that method in the message instead.
9106            if ctor == "Symbol" {
9107                let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9108                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9109                    p.insert("@@toPrimitive".into(), thunk);
9110                }
9111                self.hide_prop(&proto, "@@toPrimitive");
9112            }
9113            self.native_protos.insert(ctor.to_string(), proto);
9114        }
9115    }
9116
9117    /// The cached template object for one tagged-template site, if it has been
9118    /// evaluated before.
9119    pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9120        self.template_objects.get(&key).cloned()
9121    }
9122    /// Record the template object for one tagged-template site.
9123    pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9124        self.template_objects.insert(key, v);
9125    }
9126
9127    /// The real prototype object for a builtin exotic, if it has one.
9128    pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9129        self.native_protos.get(ctor).cloned()
9130    }
9131
9132    /// The constructor name whose `.prototype` object IS `v`, for a prototype
9133    /// this host built as a real object (`String.prototype`, `TypeError
9134    /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9135    ///
9136    /// A prototype is an ORDINARY object: it carries no instance's internal
9137    /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9138    /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9139    /// before, so the brand fell through to the "does it look like an Error"
9140    /// test and answered for the prototype as if it were an instance.
9141    pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9142        if !matches!(v, Value::Obj(_)) {
9143            return None;
9144        }
9145        self.native_protos
9146            .iter()
9147            .chain(self.error_protos.iter())
9148            .find(|(_, p)| *p == v)
9149            .map(|(name, _)| name.as_str())
9150    }
9151
9152    /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9153    /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9154    ///
9155    /// `Ctor.prototype` used to read `undefined` for every native class outside the
9156    /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9157    /// that libraries still use. `iconv-lite`'s internal codec — reached from
9158    /// `raw-body` on every `express.json()` request — does exactly this:
9159    ///
9160    /// ```text
9161    /// var StringDecoder = require('string_decoder').StringDecoder;
9162    /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9163    /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9164    /// InternalDecoder.prototype = StringDecoder.prototype;
9165    /// ```
9166    ///
9167    /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9168    ///
9169    /// Methods come from `stdlib::instance_method_lists`, the same table a method
9170    /// READ consults, so the prototype can never advertise a name the dispatcher
9171    /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9172    /// dispatches against its invoke-time `this`, so a subclass instance whose
9173    /// prototype IS this object gets the native implementation. Returns `None` for
9174    /// a tag with no instance methods, leaving those constructors as they were.
9175    pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9176        if let Some(p) = self.native_protos.get(ctor) {
9177            return Some(p.clone());
9178        }
9179        // `Buffer` and the typed-array kinds belong to the chain
9180        // `ensure_native_protos` builds. Building one of them here first hung it
9181        // straight off `Object.prototype` AND registered it, which made that
9182        // chain's own `contains_key("Buffer")` guard skip the build for the rest
9183        // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9184        // Uint8Array` read false.
9185        if ctor == "Buffer"
9186            || ctor == "TypedArray"
9187            || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9188        {
9189            self.ensure_native_protos();
9190            return self.native_protos.get(ctor).cloned();
9191        }
9192        let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9193        // A class can carry accessors and no methods at all
9194        // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9195        // `asymmetricKeyDetails`), so an empty method list does not mean there
9196        // is no prototype to build.
9197        let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9198        if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9199            return None;
9200        }
9201        // A native class with a real PARENT hangs off that parent's prototype
9202        // rather than straight off `Object.prototype`. The stream hierarchy is
9203        // `Readable → Stream → EventEmitter`, which is what makes
9204        // `new Readable() instanceof Stream` hold and what an ES5 subclass
9205        // doing `Object.create(Stream.prototype)` inherits from.
9206        let parent_proto = match crate::stdlib::native_parent(ctor) {
9207            Some(p) => self
9208                .ensure_ctor_proto(p)
9209                .unwrap_or_else(|| self.object_proto()),
9210            None => self.object_proto(),
9211        };
9212        let proto = self.new_object(IndexMap::new());
9213        self.set_proto(&proto, parent_proto);
9214        let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9215        if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9216            p.insert("constructor".into(), ctor_val);
9217        }
9218        self.hide_prop(&proto, "constructor");
9219        // A prototype member is ENUMERABLE in node for every class but the few
9220        // written as ES classes, so `for (const k in url)` walks `href` and the
9221        // rest. Hiding all of them made that loop find nothing.
9222        let visible = crate::stdlib::instance_members_enumerable(ctor);
9223        let symbols = crate::builtins::proto_symbol_methods(ctor);
9224        for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9225            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9226            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9227                p.insert((*m).to_string(), thunk);
9228            }
9229            // A symbol-keyed member never enumerates.
9230            if !visible || m.starts_with("@@") {
9231                self.hide_prop(&proto, m);
9232            }
9233        }
9234        // Accessors and the class's `Symbol.toStringTag`, both of which live on
9235        // the PROTOTYPE in node — an instance owns neither.
9236        let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9237        for (key, settable) in accessors {
9238            let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9239            let set =
9240                settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9241            self.set_accessor(&proto, key, Some(get), set);
9242            if !visible {
9243                self.hide_prop(&proto, key);
9244            }
9245        }
9246        for m in crate::stdlib::instance_late_methods(ctor) {
9247            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9248            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9249                p.insert((*m).to_string(), thunk);
9250            }
9251            if !visible {
9252                self.hide_prop(&proto, m);
9253            }
9254        }
9255        if !tag.is_empty() {
9256            let tag = self.new_str(tag.to_string());
9257            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9258                p.insert("@@toStringTag".into(), tag);
9259            }
9260            self.hide_prop(&proto, "@@toStringTag");
9261        }
9262        self.native_protos.insert(ctor.to_string(), proto.clone());
9263        Some(proto)
9264    }
9265
9266    /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9267    /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9268    /// which answers from `native_protos` — does not find them.
9269    pub fn error_proto(&self, name: &str) -> Option<Value> {
9270        self.error_protos.get(name).cloned()
9271    }
9272
9273    pub fn ensure_error_protos(&mut self) {
9274        if !self.error_protos.is_empty() {
9275            return;
9276        }
9277        let obj_proto = self.object_proto();
9278        // Error.prototype first (the shared base).
9279        let err_proto = self.new_object(IndexMap::new());
9280        self.set_proto(&err_proto, obj_proto);
9281        let nm = self.new_str("Error");
9282        let empty = self.new_str("");
9283        let ctor = self.alloc(JsObj::Builtin("Error".into()));
9284        // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9285        // not a fallback the stringifier applies when nothing else matches: it
9286        // exists precisely to shadow `Object.prototype.toString`. Without it,
9287        // the first read of `Error.prototype` or `Object.prototype` — which
9288        // `x instanceof Error` performs, so ordinary code triggers it —
9289        // materialised `Object.prototype.toString`, the chain lookup started
9290        // finding it, and `String(err)` flipped from `Error: m` to
9291        // `[object Error]` for the REST OF THE PROCESS, including errors
9292        // created before the read.
9293        let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9294        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9295            p.insert("name".into(), nm);
9296            p.insert("message".into(), empty);
9297            p.insert("constructor".into(), ctor);
9298            p.insert("toString".into(), to_string);
9299        }
9300        // Everything on `Error.prototype` is non-enumerable in V8.
9301        for k in ["name", "message", "constructor", "toString"] {
9302            self.hide_prop(&err_proto, k);
9303        }
9304        self.error_protos.insert("Error".into(), err_proto.clone());
9305        for name in &ERROR_NAMES[1..] {
9306            let p = self.new_object(IndexMap::new());
9307            self.set_proto(&p, err_proto.clone());
9308            let nm = self.new_str(*name);
9309            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9310            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9311                o.insert("name".into(), nm);
9312                o.insert("constructor".into(), ctor);
9313            }
9314            self.hide_prop(&p, "name");
9315            self.hide_prop(&p, "constructor");
9316            self.error_protos.insert((*name).to_string(), p);
9317        }
9318    }
9319}
9320
9321// ── Map/Set element access (used by builtins) ────────────────────────────────
9322
9323impl JsHost {
9324    /// A function's `.length`: the count of leading params before the first one
9325    /// with a default or the rest element.
9326    pub fn func_arity(&self, v: &Value) -> usize {
9327        // 20.2.3.2: a bound function's `length` is the target's, less the
9328        // arguments already bound, floored at 0. Reporting 0 for every bound
9329        // function breaks arity dispatch — express picks error-handling
9330        // middleware with `fn.length === 4`, so a bound handler was never
9331        // recognised as one.
9332        if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9333            return self.func_arity(&target.clone()).saturating_sub(args.len());
9334        }
9335        // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9336        // reports 1 rather than the 0 a target of unknown arity would give.
9337        if let Some(JsObj::Builtin(n)) = self.get(v) {
9338            return crate::builtins::builtin_meta(n)
9339                .map(|(_, len)| len as usize)
9340                .unwrap_or(0);
9341        }
9342        let def_id = match self.get(v) {
9343            Some(JsObj::Func(f)) => Some(f.def_id),
9344            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9345                Some(JsObj::Func(f)) => Some(f.def_id),
9346                _ => None,
9347            },
9348            _ => None,
9349        };
9350        match def_id.and_then(|id| self.funcs.get(id)) {
9351            Some(def) => def
9352                .params
9353                .iter()
9354                .take_while(|p| !p.rest && !p.has_default)
9355                .count(),
9356            None => 0,
9357        }
9358    }
9359
9360    pub fn is_map(&self, v: &Value) -> bool {
9361        matches!(self.get(v), Some(JsObj::Map { .. }))
9362    }
9363    pub fn is_set(&self, v: &Value) -> bool {
9364        matches!(self.get(v), Some(JsObj::Set { .. }))
9365    }
9366}
9367
9368// ── promises & the event loop ────────────────────────────────────────────────
9369
9370impl JsHost {
9371    /// Allocate a fresh pending promise, returning its heap value.
9372    pub fn new_promise(&mut self) -> Value {
9373        let id = self.promises.len() as u32;
9374        self.promises.push(PromiseCell {
9375            state: PromiseState::Pending,
9376            value: Value::Undef,
9377            reactions: Vec::new(),
9378            handled: false,
9379        });
9380        self.alloc(JsObj::Promise { id })
9381    }
9382    pub fn promise_id(&self, v: &Value) -> Option<u32> {
9383        match self.get(v) {
9384            Some(JsObj::Promise { id }) => Some(*id),
9385            _ => None,
9386        }
9387    }
9388    pub fn promise_state(&self, id: u32) -> PromiseState {
9389        self.promises[id as usize].state
9390    }
9391    pub fn promise_value(&self, id: u32) -> Value {
9392        self.promises[id as usize].value.clone()
9393    }
9394    pub fn promise_mark_handled(&mut self, id: u32) {
9395        self.promises[id as usize].handled = true;
9396    }
9397    /// Take the pending reactions of a promise (called on settle).
9398    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9399        std::mem::take(&mut self.promises[id as usize].reactions)
9400    }
9401    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9402        self.promises[id as usize].reactions.push(r);
9403    }
9404    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9405        let c = &mut self.promises[id as usize];
9406        if c.state != PromiseState::Pending {
9407            return; // already settled — resolve/reject are one-shot
9408        }
9409        c.state = state;
9410        c.value = value;
9411    }
9412    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9413        self.microtasks.push_back(Task::Js { cb, args });
9414    }
9415    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9416        self.nextticks.push_back(Task::Js { cb, args });
9417    }
9418    /// Schedule a native (Rust) microtask — used by Promise reactions and async
9419    /// resumption.
9420    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9421        self.microtasks.push_back(Task::Native(f));
9422    }
9423    /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9424    /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9425    /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9426    /// `ref`/`unref` and `refresh` can find this entry again.
9427    pub fn add_timer(
9428        &mut self,
9429        delay: f64,
9430        callback: Value,
9431        args: Vec<Value>,
9432        interval: Option<f64>,
9433    ) -> u64 {
9434        let id = self.next_timer;
9435        self.next_timer += 1;
9436        // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9437        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9438        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9439        self.macrotasks.push(Timer {
9440            id,
9441            delay,
9442            seq: id,
9443            callback,
9444            args,
9445            cancelled: false,
9446            interval,
9447            refed: true,
9448            deadline,
9449        });
9450        id
9451    }
9452    /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9453    /// `clearInterval` from *inside* the callback cancels this very entry) and
9454    /// taking a fresh `seq` so same-delay peers still round-robin.
9455    ///
9456    /// Called BEFORE the callback runs: if it were called after, the entry would
9457    /// be absent while the callback executed and a `clearInterval(t)` there would
9458    /// cancel nothing, resurrecting an interval the program had stopped.
9459    fn rearm_timer(&mut self, t: &Timer, period: f64) {
9460        let seq = self.next_timer;
9461        self.next_timer += 1;
9462        let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9463        self.macrotasks.push(Timer {
9464            id: t.id,
9465            delay: t.delay,
9466            seq,
9467            callback: t.callback.clone(),
9468            args: t.args.clone(),
9469            cancelled: false,
9470            interval: Some(period),
9471            refed: t.refed,
9472            deadline,
9473        });
9474    }
9475    /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9476    /// timer. A no-op once the timer has fired or been cleared (Node likewise
9477    /// treats `ref`/`unref` on a dead timer as inert).
9478    pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9479        for t in &mut self.macrotasks {
9480            if t.id == id && !t.cancelled {
9481                t.refed = refed;
9482            }
9483        }
9484    }
9485    /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9486    /// A fired or cleared timer reports `false`, matching Node.
9487    pub fn timer_has_ref(&self, id: u64) -> bool {
9488        self.macrotasks
9489            .iter()
9490            .any(|t| t.id == id && !t.cancelled && t.refed)
9491    }
9492    /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9493    /// just been scheduled.
9494    pub fn refresh_timer(&mut self, id: u64) {
9495        let now = Instant::now();
9496        for t in &mut self.macrotasks {
9497            if t.id == id && !t.cancelled {
9498                t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9499            }
9500        }
9501    }
9502    /// Clone the I/O sender for a background I/O thread.
9503    pub fn io_sender(&self) -> Sender<IoTask> {
9504        self.io_tx.clone()
9505    }
9506    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9507    /// alive.
9508    pub fn incr_handle(&mut self) {
9509        self.open_handles += 1;
9510    }
9511    /// Release a handle; the loop exits once this reaches `0` with empty queues.
9512    pub fn decr_handle(&mut self) {
9513        self.open_handles = self.open_handles.saturating_sub(1);
9514    }
9515    pub fn open_handles(&self) -> usize {
9516        self.open_handles
9517    }
9518    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9519    /// path). Ties break by `seq`.
9520    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9521        let idx = self
9522            .macrotasks
9523            .iter()
9524            .enumerate()
9525            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9526            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9527            .map(|(i, _)| i);
9528        idx.map(|i| self.macrotasks.remove(i))
9529    }
9530    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9531    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9532    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9533        self.macrotasks
9534            .iter()
9535            .filter(|t| !t.cancelled)
9536            .map(|t| t.deadline)
9537            .min()
9538            .map(|d| d.saturating_duration_since(now))
9539    }
9540    pub fn cancel_timer(&mut self, id: u64) {
9541        for t in &mut self.macrotasks {
9542            if t.id == id {
9543                t.cancelled = true;
9544            }
9545        }
9546    }
9547    fn pop_next_timer(&mut self) -> Option<Timer> {
9548        // Earliest (delay, seq) fires first — a deterministic virtual clock.
9549        let idx = self
9550            .macrotasks
9551            .iter()
9552            .enumerate()
9553            .filter(|(_, t)| !t.cancelled)
9554            .min_by(|(_, a), (_, b)| {
9555                a.delay
9556                    .partial_cmp(&b.delay)
9557                    .unwrap_or(std::cmp::Ordering::Equal)
9558                    .then(a.seq.cmp(&b.seq))
9559            })
9560            .map(|(i, _)| i);
9561        idx.map(|i| self.macrotasks.remove(i))
9562    }
9563    fn next_microtask(&mut self) -> Option<Task> {
9564        // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9565        // queue, then drain the microtask queue in full, then repeat if the
9566        // microtasks queued more ticks. A tick queued from INSIDE a microtask
9567        // therefore waits for the rest of that microtask queue.
9568        //
9569        // Preferring ticks on every step interleaved the two, so
9570        // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9571        // promise callbacks queued behind it — the one ordering difference a
9572        // library scheduling work from a `.then` can actually observe.
9573        if !self.draining_micro {
9574            if let Some(t) = self.nextticks.pop_front() {
9575                return Some(t);
9576            }
9577        }
9578        if let Some(t) = self.microtasks.pop_front() {
9579            // Stay in the microtask phase until this queue is exhausted.
9580            self.draining_micro = !self.microtasks.is_empty();
9581            return Some(t);
9582        }
9583        self.draining_micro = false;
9584        self.nextticks.pop_front()
9585    }
9586    fn has_microtasks(&self) -> bool {
9587        !self.nextticks.is_empty() || !self.microtasks.is_empty()
9588    }
9589    /// Whether any pending timer is *referenced* — the timer half of Node's
9590    /// handle count. Only these keep the loop alive; unref'd timers still fire
9591    /// while something else holds the loop open, but never hold it themselves.
9592    fn has_refed_macrotasks(&self) -> bool {
9593        self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9594    }
9595    /// Whether any pending timer repeats. A repeating timer cannot run on the
9596    /// virtual clock: virtual time never advances, so the interval would re-arm
9597    /// at the same instant forever, spinning a core and starving every
9598    /// longer-delay timer behind it. Its presence forces the real clock.
9599    fn has_pending_interval(&self) -> bool {
9600        self.macrotasks
9601            .iter()
9602            .any(|t| !t.cancelled && t.interval.is_some())
9603    }
9604}
9605
9606/// Drive the event loop to quiescence.
9607///
9608/// **Liveness** is Node's handle count: the loop runs while a microtask is
9609/// pending, an open handle is registered (a listening server, a live socket, an
9610/// in-flight async op), or a *referenced* timer is still pending. That last term
9611/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9612/// does in Node — the interval re-arms itself, so a ref'd timer is always
9613/// pending and the loop never reaches its exit condition.
9614///
9615/// Two **clock regimes**, selected per iteration:
9616///
9617/// - **Virtual clock** (no open handles and no repeating timer): the original
9618///   deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9619///   real waiting. Parity output and test speed for ordinary `setTimeout`
9620///   scripts are unchanged.
9621/// - **Real clock** (an open handle, or any pending interval): fire every timer
9622///   whose wall-clock deadline has passed, then BLOCK on the I/O channel
9623///   (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9624///   timers) and run the received `IoTask` on the main thread. The host keeps
9625///   its own `Sender`, so `recv` never disconnects while the process should stay
9626///   alive.
9627///
9628///   A repeating timer *must* take this path: virtual time never advances, so an
9629///   interval on the virtual clock would re-fire at the same instant forever,
9630///   spinning a core and starving every longer-delay timer behind it.
9631///
9632/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9633pub fn run_event_loop() -> Result<(), String> {
9634    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9635    // host borrow); restore it afterward so a re-entrant run reuses the channel.
9636    let rx = with_host(|h| h.io_rx.take());
9637    let result = drive_event_loop(rx.as_ref());
9638    with_host(|h| h.io_rx = rx);
9639    result
9640}
9641
9642fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9643    loop {
9644        // 1) Exhaust the microtask queue (nextTick before promise reactions),
9645        //    then report anything that rejected with nobody watching.
9646        while let Some(task) = with_host(|h| h.next_microtask()) {
9647            task.run()?;
9648        }
9649        check_unhandled_rejections()?;
9650
9651        // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9652        //    process exits, dropping any unref'd timers still pending — which is
9653        //    why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9654        //    timer behind a ref'd one does.
9655        let alive =
9656            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9657        if !alive {
9658            break;
9659        }
9660
9661        // 3) Pick the clock regime for this turn.
9662        let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9663        if virtual_clock {
9664            // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9665            match with_host(|h| h.pop_next_timer()) {
9666                Some(t) => fire_timer(t)?,
9667                // Unreachable while `alive` holds (a ref'd timer must exist),
9668                // but exiting is the safe reading of "nothing left to run".
9669                None => break,
9670            }
9671            continue;
9672        }
9673
9674        // ── real-clock / blocking-I/O regime ─────────────────────────────────
9675        let now = Instant::now();
9676        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9677            fire_timer(t)?;
9678            continue; // re-drain microtasks, re-check deadlines
9679        }
9680        // Nothing due and no pending microtasks: block for the next I/O event,
9681        // bounded by the soonest timer deadline so due timers still fire on time.
9682        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9683        let timeout = with_host(|h| h.next_timer_timeout(now));
9684        let recv = match timeout {
9685            Some(d) => rx.recv_timeout(d),
9686            None => rx
9687                .recv()
9688                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9689        };
9690        match recv {
9691            Ok(task) => task()?,
9692            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9693            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9694        }
9695    }
9696    Ok(())
9697}
9698
9699/// Run one due timer's callback, first re-arming it if it repeats.
9700///
9701/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9702/// issued from inside that callback cancels the next occurrence. Re-arming
9703/// afterwards would leave the interval absent from the queue for the duration of
9704/// its own callback, so the `clear` would match nothing and the freshly pushed
9705/// entry would resurrect an interval the program had just stopped.
9706fn fire_timer(t: Timer) -> Result<(), String> {
9707    if let Some(period) = t.interval {
9708        with_host(|h| h.rearm_timer(&t, period));
9709    }
9710    invoke(&t.callback, t.args, None)?;
9711    Ok(())
9712}
9713
9714// ── async functions & promise resolution (native) ────────────────────────────
9715
9716/// Drive a freshly-built async coroutine and return its result promise.
9717fn run_async(gen: Value) -> Value {
9718    let result = with_host(|h| h.new_promise());
9719    let rid = with_host(|h| h.promise_id(&result).unwrap());
9720    drive_async(gen, rid, Value::Undef);
9721    result
9722}
9723
9724/// Resume an async coroutine one step, wiring `await` continuations to promise
9725/// settlement.
9726fn drive_async(gen: Value, rid: u32, send: Value) {
9727    match gen_resume(&gen, send) {
9728        Ok(GenStep::Yield(awaited)) => {
9729            let ap = promise_of(&awaited);
9730            let aid = with_host(|h| h.promise_id(&ap).unwrap());
9731            let gen2 = gen.clone();
9732            subscribe_native(
9733                aid,
9734                Box::new(move |state, val| {
9735                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
9736                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9737                    let tag = if state == PromiseState::Rejected {
9738                        1.0
9739                    } else {
9740                        0.0
9741                    };
9742                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9743                    drive_async(gen2, rid, packet);
9744                    Ok(())
9745                }),
9746            );
9747        }
9748        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9749        Err(e) => {
9750            let ev = take_exc_or_error(&e);
9751            reject_promise_val(rid, ev);
9752        }
9753    }
9754}
9755
9756/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9757/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9758pub fn await_value(awaited: Value) -> Result<Value, String> {
9759    // Inside an `async function*`, `await` and `yield` share one coroutine
9760    // yielder, so an awaited value has to be tagged or the driver would hand it
9761    // to the consumer as if the body had yielded it.
9762    let awaited = match CUR_GEN.with(|c| c.get()) {
9763        Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9764            let mut m = IndexMap::new();
9765            m.insert(AWAIT_MARKER.to_string(), awaited);
9766            h.new_object(m)
9767        }),
9768        _ => awaited,
9769    };
9770    let packet = gen_yield(awaited)?;
9771    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9772    let tag = items
9773        .first()
9774        .map(|v| with_host(|h| h.to_number(v)))
9775        .unwrap_or(0.0);
9776    let val = items.get(1).cloned().unwrap_or(Value::Undef);
9777    if tag == 1.0 {
9778        with_host(|h| h.exc = Some(val.clone()));
9779        Err(with_host(|h| crate::builtins::error_string(h, &val)))
9780    } else {
9781        Ok(val)
9782    }
9783}
9784
9785/// Hidden key marking an `await` suspension inside an async generator.
9786const AWAIT_MARKER: &str = "@@await";
9787
9788/// The operand of an `await` suspension, or `None` for a real `yield`.
9789fn await_marker(v: &Value) -> Option<Value> {
9790    with_host(|h| match h.get(v) {
9791        Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9792        _ => None,
9793    })
9794}
9795
9796/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9797/// hand back the promise its `{value, done}` record (or rejection) will settle.
9798///
9799/// All three of `.next`, `.return` and `.throw` come through here, so a request
9800/// never resumes the body while an earlier one is still suspended on an
9801/// internal `await`.
9802pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9803    let step = with_host(|h| h.new_promise());
9804    let sid = with_host(|h| h.promise_id(&step).unwrap());
9805    let id = match with_host(|h| match h.get(gen) {
9806        Some(JsObj::Generator { id }) => Some(*id),
9807        _ => None,
9808    }) {
9809        Some(id) => id,
9810        None => return step,
9811    };
9812    with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9813    pump_async_gen(gen.clone(), id);
9814    step
9815}
9816
9817/// One `.next(v)` of an `async function*`.
9818pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9819    async_gen_enqueue(gen, GenReq::Next(send))
9820}
9821
9822/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9823/// already in flight (the body may only be resumed by one request at a time).
9824fn pump_async_gen(gen: Value, id: u32) {
9825    if with_host(|h| h.generators[id as usize].running) {
9826        return;
9827    }
9828    let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9829        return;
9830    };
9831    with_host(|h| h.generators[id as usize].running = true);
9832    start_async_gen_req(gen, sid, req);
9833}
9834
9835/// Begin one queued request: resume the body with the completion it carries,
9836/// then hand the outcome to the shared continuation.
9837///
9838/// A RETURN completion always Awaits its value before the body sees it — via
9839/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9840/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9841/// when it is not yet started or already completed. So a `.return()` settles one
9842/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9843/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9844/// `.return()` overtake the reactions of the `.next()` it followed.
9845fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9846    if matches!(req, GenReq::Return(_)) {
9847        with_host(|h| {
9848            h.queue_micro_native(Box::new(move || {
9849                resume_async_gen_req(gen, sid, req);
9850                Ok(())
9851            }))
9852        });
9853        return;
9854    }
9855    resume_async_gen_req(gen, sid, req);
9856}
9857
9858/// Deliver a queued completion to the body and settle its step promise.
9859fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9860    let step = match req {
9861        GenReq::Next(v) => gen_resume(&gen, v),
9862        GenReq::Return(v) => gen_return(&gen, v),
9863        GenReq::Throw(e) => gen_throw(&gen, e),
9864    };
9865    settle_async_gen_step(gen, sid, step);
9866}
9867
9868/// One request has settled: release the body and start the next queued request.
9869fn finish_async_gen_step(gen: Value, id: u32) {
9870    with_host(|h| h.generators[id as usize].running = false);
9871    pump_async_gen(gen, id);
9872}
9873
9874/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9875pub fn is_async_generator(v: &Value) -> bool {
9876    let id = match with_host(|h| match h.get(v) {
9877        Some(JsObj::Generator { id }) => Some(*id),
9878        _ => None,
9879    }) {
9880        Some(id) => id,
9881        None => return false,
9882    };
9883    with_host(|h| h.generators[id as usize].async_gen)
9884}
9885
9886/// A `{ value, done }` iterator-result object.
9887fn iter_record(value: Value, done: bool) -> Value {
9888    with_host(|h| {
9889        let mut m = IndexMap::new();
9890        m.insert("value".to_string(), value);
9891        m.insert("done".to_string(), Value::Bool(done));
9892        h.new_object(m)
9893    })
9894}
9895
9896/// Resume a request that was suspended on an internal `await` (always a normal
9897/// completion — the awaited promise's outcome rides in `packet`).
9898fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9899    let step = gen_resume(&gen, packet);
9900    settle_async_gen_step(gen, sid, step);
9901}
9902
9903/// Turn one body resumption into a settled step promise: transparently re-drive
9904/// internal `await` suspensions, and settle on the first REAL yield or on the
9905/// body's completion. Shared by the initial resume of a queued request and by
9906/// every await-resumption of it.
9907fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9908    let id = match with_host(|h| match h.get(&gen) {
9909        Some(JsObj::Generator { id }) => Some(*id),
9910        _ => None,
9911    }) {
9912        Some(id) => id,
9913        None => return,
9914    };
9915    match step {
9916        Ok(GenStep::Yield(v)) => match await_marker(&v) {
9917            Some(awaited) => {
9918                // An internal `await`: settle it, then resume the body. The
9919                // request stays in flight across the suspension.
9920                let ap = promise_of(&awaited);
9921                let aid = with_host(|h| h.promise_id(&ap).unwrap());
9922                subscribe_native(
9923                    aid,
9924                    Box::new(move |state, val| {
9925                        let tag = if state == PromiseState::Rejected {
9926                            1.0
9927                        } else {
9928                            0.0
9929                        };
9930                        let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9931                        drive_async_gen(gen.clone(), sid, packet);
9932                        Ok(())
9933                    }),
9934                );
9935            }
9936            // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
9937            // AWAITED before the step promise settles, so `yield somePromise`
9938            // hands the consumer the RESOLVED value (and costs its microtask).
9939            None => {
9940                let yp = promise_of(&v);
9941                let yid = with_host(|h| h.promise_id(&yp).unwrap());
9942                subscribe_native(
9943                    yid,
9944                    Box::new(move |state, val| {
9945                        if state == PromiseState::Rejected {
9946                            reject_promise_val(sid, val);
9947                        } else {
9948                            resolve_promise_val(sid, iter_record(val, false));
9949                        }
9950                        finish_async_gen_step(gen.clone(), id);
9951                        Ok(())
9952                    }),
9953                );
9954            }
9955        },
9956        Ok(GenStep::Done(v)) => {
9957            resolve_promise_val(sid, iter_record(v, true));
9958            finish_async_gen_step(gen, id);
9959        }
9960        Err(e) => {
9961            let ev = take_exc_or_error(&e);
9962            reject_promise_val(sid, ev);
9963            finish_async_gen_step(gen, id);
9964        }
9965    }
9966}
9967
9968/// A promise for `v`: `v` itself if it is already a promise, else a promise
9969/// resolved with `v`.
9970pub fn promise_of(v: &Value) -> Value {
9971    if with_host(|h| h.promise_id(v)).is_some() {
9972        return v.clone();
9973    }
9974    let p = with_host(|h| h.new_promise());
9975    let id = with_host(|h| h.promise_id(&p).unwrap());
9976    resolve_promise_val(id, v.clone());
9977    p
9978}
9979
9980/// Register a native reaction on promise `id` (schedules immediately if already
9981/// settled).
9982pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
9983    // A native continuation (`await`, promise adoption, `for await`) observes a
9984    // rejection exactly as a `.catch` does, so it is not "unhandled".
9985    with_host(|h| h.promise_mark_handled(id));
9986    let state = with_host(|h| h.promise_state(id));
9987    if state == PromiseState::Pending {
9988        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
9989    } else {
9990        let val = with_host(|h| h.promise_value(id));
9991        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
9992    }
9993}
9994
9995/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
9996/// else fulfill with it.
9997pub fn resolve_promise_val(id: u32, value: Value) {
9998    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9999        return;
10000    }
10001    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10002        if vid == id {
10003            // Resolving a promise with itself → reject with a TypeError.
10004            let e = with_host(|h| {
10005                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10006            });
10007            reject_promise_val(id, e);
10008            return;
10009        }
10010        // A native promise is still a thenable, so the spec routes it through
10011        // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10012        // is even registered. (`await` does NOT pay this: V8's await optimization
10013        // subscribes to a native promise directly, which `await_value` mirrors.)
10014        with_host(|h| {
10015            h.queue_micro_native(Box::new(move || {
10016                subscribe_native(
10017                    vid,
10018                    Box::new(move |state, val| {
10019                        with_host(|h| h.settle_promise(id, state, val.clone()));
10020                        schedule_reactions(id);
10021                        Ok(())
10022                    }),
10023                );
10024                Ok(())
10025            }))
10026        });
10027        return;
10028    }
10029    // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10030    // through a dedicated job — the promise adopts what `then` reports, it is
10031    // never fulfilled WITH the thenable itself.
10032    if let Some(then) = thenable_then(&value) {
10033        with_host(|h| {
10034            h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10035        });
10036        return;
10037    }
10038    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10039    schedule_reactions(id);
10040}
10041
10042/// `value.then` if `value` is an object with a callable `then` — the test that
10043/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10044fn thenable_then(value: &Value) -> Option<Value> {
10045    // A PROXY is not a plain object and supplies `then` through its `get` trap,
10046    // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10047    // WITH the proxy instead of adopting it.
10048    if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10049        return protocol_lookup(value, "then")
10050            .ok()
10051            .flatten()
10052            .filter(|f| with_host(|h| is_callable(h, f)));
10053    }
10054    if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10055        return None;
10056    }
10057    let then = with_host(|h| lookup_chain(h, value, "then"))?;
10058    with_host(|h| is_callable(h, &then)).then_some(then)
10059}
10060
10061/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10062/// reject continuations and let it settle us. A throw out of `then` rejects.
10063fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10064    let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10065    let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10066    if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10067        let ev = take_exc_or_error(&e);
10068        reject_promise_val(id, ev);
10069    }
10070    Ok(())
10071}
10072
10073pub fn reject_promise_val(id: u32, value: Value) {
10074    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10075        return;
10076    }
10077    with_host(|h| {
10078        h.settle_promise(id, PromiseState::Rejected, value);
10079        h.pending_rejections.push(id);
10080    });
10081    schedule_reactions(id);
10082}
10083
10084/// Report every promise that settled rejected since the last checkpoint and
10085/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10086/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10087/// `process.on('unhandledRejection')` listener takes it.
10088fn check_unhandled_rejections() -> Result<(), String> {
10089    loop {
10090        let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10091        if ids.is_empty() {
10092            return Ok(());
10093        }
10094        for id in ids {
10095            let unhandled = with_host(|h| {
10096                h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10097            });
10098            if !unhandled {
10099                continue;
10100            }
10101            // Report each promise at most once, however many checkpoints pass.
10102            with_host(|h| h.promise_mark_handled(id));
10103            let val = with_host(|h| h.promise_value(id));
10104            let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10105            if listeners.is_empty() {
10106                let msg = with_host(|h| crate::builtins::error_string(h, &val));
10107                with_host(|h| h.exc = Some(val));
10108                return Err(msg);
10109            }
10110            let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10111            for f in listeners {
10112                invoke(&f, vec![val.clone(), promise.clone()], None)?;
10113            }
10114        }
10115    }
10116}
10117
10118/// Drain a settled promise's reactions into microtasks.
10119fn schedule_reactions(id: u32) {
10120    let reactions = with_host(|h| h.take_reactions(id));
10121    let state = with_host(|h| h.promise_state(id));
10122    let value = with_host(|h| h.promise_value(id));
10123    for r in reactions {
10124        let value = value.clone();
10125        match r {
10126            PromiseReaction::Native(f) => {
10127                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10128            }
10129            PromiseReaction::Js {
10130                on_ful,
10131                on_rej,
10132                result,
10133            } => {
10134                with_host(|h| {
10135                    h.queue_micro_native(Box::new(move || {
10136                        run_js_reaction(state, value, on_ful, on_rej, result)
10137                    }))
10138                });
10139            }
10140        }
10141    }
10142}
10143
10144/// Run a `.then` reaction: call the appropriate handler and settle the result
10145/// promise with its outcome (or pass through if there is no handler).
10146fn run_js_reaction(
10147    state: PromiseState,
10148    value: Value,
10149    on_ful: Value,
10150    on_rej: Value,
10151    result: Value,
10152) -> Result<(), String> {
10153    let rid = match with_host(|h| h.promise_id(&result)) {
10154        Some(i) => i,
10155        None => return Ok(()),
10156    };
10157    let handler = if state == PromiseState::Rejected {
10158        on_rej
10159    } else {
10160        on_ful
10161    };
10162    if with_host(|h| is_callable(h, &handler)) {
10163        match invoke(&handler, vec![value], None) {
10164            Ok(r) => resolve_promise_val(rid, r),
10165            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10166        }
10167    } else if state == PromiseState::Rejected {
10168        reject_promise_val(rid, value);
10169    } else {
10170        resolve_promise_val(rid, value);
10171    }
10172    Ok(())
10173}
10174
10175/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10176/// synthesized `Error` from the internal message.
10177pub fn take_exc_or_error(e: &str) -> Value {
10178    with_host(|h| {
10179        h.error.take();
10180        h.exc
10181            .take()
10182            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10183    })
10184}
10185
10186/// Register a user `.then` reaction (JS handlers + result promise).
10187pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10188    let id = match with_host(|h| h.promise_id(p)) {
10189        Some(i) => i,
10190        None => return Value::Undef,
10191    };
10192    with_host(|h| h.promise_mark_handled(id));
10193    // The result is built with the RECEIVER's species, so a subclass promise
10194    // stays a subclass promise through a `.then` chain.
10195    let result = match crate::builtins::promise_species_from(p) {
10196        Ok(Some(sp)) => sp,
10197        _ => with_host(|h| h.new_promise()),
10198    };
10199    let reaction = PromiseReaction::Js {
10200        on_ful,
10201        on_rej,
10202        result: result.clone(),
10203    };
10204    let state = with_host(|h| h.promise_state(id));
10205    if state == PromiseState::Pending {
10206        with_host(|h| h.add_reaction(id, reaction));
10207    } else {
10208        let value = with_host(|h| h.promise_value(id));
10209        if let PromiseReaction::Js {
10210            on_ful,
10211            on_rej,
10212            result,
10213        } = reaction
10214        {
10215            with_host(|h| {
10216                h.queue_micro_native(Box::new(move || {
10217                    run_js_reaction(state, value, on_ful, on_rej, result)
10218                }))
10219            });
10220        }
10221    }
10222    result
10223}
10224
10225/// The ReferenceError for touching `this` in a derived constructor before
10226/// `super()` — or returning from one without calling it.
10227pub fn this_before_super_error() -> String {
10228    "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10229}
10230
10231/// The intrinsic a builtin name denotes. Two property paths that the spec
10232/// defines as the SAME function object compare `===`: `Number.parseInt` is
10233/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10234/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10235fn builtin_identity(name: &str) -> &str {
10236    match name {
10237        "Number.parseInt" => "parseInt",
10238        "Number.parseFloat" => "parseFloat",
10239        _ => name,
10240    }
10241}