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/// What an array iterator yields at each index: `keys()`, `values()` (and
421/// `Symbol.iterator`), or `entries()`.
422#[derive(Clone, Copy, PartialEq, Eq)]
423pub enum ArrayIterKind {
424    Keys,
425    Values,
426    Entries,
427}
428
429/// A heap object.
430#[derive(Clone)]
431pub enum JsObj {
432    Str(String),
433    Array(Vec<Value>),
434    Object(IndexMap<String, Value>),
435    Func(FuncVal),
436    /// A first-class reference to a builtin function or namespace
437    /// (`console.log`, `Math`, `parseInt`).
438    Builtin(String),
439    /// A bound method value (`obj.method` captured then called): dispatches
440    /// through `call_method(recv, name, args)` when invoked.
441    BoundMethod {
442        recv: Value,
443        name: String,
444    },
445    /// The single canonical `null`.
446    Null,
447    /// An iterator over a sequence, with a cursor.
448    ///
449    /// `items` is a snapshot, taken when the iterator was made. An ARRAY
450    /// iterator is not one: `array` names the array and what each step yields,
451    /// and every step reads the array as it is then (23.1.5.1), so a `for-of`
452    /// sees an element pushed or written during the loop and stops at a length
453    /// that shrank. `items` is empty for those.
454    Iter {
455        items: Vec<Value>,
456        idx: usize,
457        array: Option<(Value, ArrayIterKind)>,
458    },
459    /// A bound function (`fn.bind(thisArg, ...preargs)`).
460    BoundFunc {
461        target: Value,
462        this: Value,
463        args: Vec<Value>,
464    },
465    /// A class constructor value: the runtime object produced by a `class`.
466    Class(ClassVal),
467    /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
468    /// key (shared) vs a fresh `Symbol()`.
469    Symbol {
470        desc: Option<String>,
471        id: u64,
472    },
473    /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
474    Map {
475        entries: IndexMap<MapKey, (Value, Value)>,
476        weak: bool,
477    },
478    /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
479    Set {
480        entries: IndexMap<MapKey, Value>,
481        weak: bool,
482    },
483    /// A live generator, backed by a stackful `corosensei` coroutine in
484    /// `JsHost.generators`.
485    Generator {
486        id: u32,
487    },
488    /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
489    Promise {
490        id: u32,
491    },
492    /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
493    BigInt(num_bigint::BigInt),
494    /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
495    RegExp(Box<RegExpObj>),
496    /// A `Proxy`: every essential internal method is diverted to `handler`'s
497    /// traps (see `crate::proxy`). `revoked` is set by the thunk
498    /// `Proxy.revocable` hands back, after which every operation throws.
499    Proxy {
500        target: Value,
501        handler: Value,
502        revoked: bool,
503    },
504}
505
506/// Which variant a heap object is, carrying none of its contents.
507///
508/// Property access has to pick a branch by variant, but the code inside a branch
509/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
510/// hold a `&JsObj` borrow across the match. The way out used to be
511/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
512/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
513/// property read O(len) and any loop over a collection O(n^2). This type is the
514/// same discriminant with nothing attached, so the probe is O(1) and each branch
515/// re-borrows for only the one field it actually needs.
516/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
517/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
518/// under the sentinel string `@@<name>` (`property_key`) so the internal
519/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
520/// table walk. Symbols V8 defines but node-js does not act on are deliberately
521/// absent: a symbol that reads back while the operator it names ignores it would
522/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
523pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
524    "iterator",
525    "asyncIterator",
526    "toPrimitive",
527    "toStringTag",
528    "hasInstance",
529    // Nine more the table was missing entirely, so `Symbol.species` and friends
530    // read `undefined` and no protocol keyed on them could be expressed.
531    "species",
532    "isConcatSpreadable",
533    "match",
534    "matchAll",
535    "replace",
536    "search",
537    "split",
538    "unscopables",
539    "dispose",
540    "asyncDispose",
541];
542
543/// Whether the internal key `k` came from a SYMBOL used as a property key
544/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
545/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
546/// is an observable JavaScript property.
547pub fn is_symbol_key(k: &str) -> bool {
548    match k.strip_prefix("@@") {
549        Some(rest) => rest
550            .strip_prefix("sym:")
551            .map(|i| i.parse::<u64>().is_ok())
552            .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
553        None => false,
554    }
555}
556
557#[derive(Clone, Copy, PartialEq, Eq, Debug)]
558pub enum ObjKind {
559    Str,
560    Array,
561    Object,
562    Func,
563    Builtin,
564    BoundMethod,
565    Null,
566    Iter,
567    BoundFunc,
568    Class,
569    Symbol,
570    Map,
571    Set,
572    Generator,
573    Promise,
574    BigInt,
575    RegExp,
576    Proxy,
577}
578
579impl JsObj {
580    /// This object's variant, without touching its contents.
581    pub fn kind(&self) -> ObjKind {
582        match self {
583            JsObj::Str(_) => ObjKind::Str,
584            JsObj::Array(_) => ObjKind::Array,
585            JsObj::Object(_) => ObjKind::Object,
586            JsObj::Func(_) => ObjKind::Func,
587            JsObj::Builtin(_) => ObjKind::Builtin,
588            JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
589            JsObj::Null => ObjKind::Null,
590            JsObj::Iter { .. } => ObjKind::Iter,
591            JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
592            JsObj::Class(_) => ObjKind::Class,
593            JsObj::Symbol { .. } => ObjKind::Symbol,
594            JsObj::Map { .. } => ObjKind::Map,
595            JsObj::Set { .. } => ObjKind::Set,
596            JsObj::Generator { .. } => ObjKind::Generator,
597            JsObj::Promise { .. } => ObjKind::Promise,
598            JsObj::BigInt(_) => ObjKind::BigInt,
599            JsObj::RegExp(_) => ObjKind::RegExp,
600            JsObj::Proxy { .. } => ObjKind::Proxy,
601        }
602    }
603}
604
605/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
606/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
607/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
608/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
609#[derive(Clone)]
610pub struct RegExpObj {
611    /// The translated regex. Construction of a pattern fancy-regex still cannot
612    /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
613    /// `RegExpObj` always holds a compiled engine.
614    ///
615    /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
616    /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
617    /// per-object mutable state — while the compiled engine behind it is
618    /// immutable and identical every time. See `regexp::compiled`.
619    pub re: std::rc::Rc<fancy_regex::Regex>,
620    pub source: String,
621    pub flags: String,
622    pub global: bool,
623    pub ignore_case: bool,
624    pub multiline: bool,
625    pub dot_all: bool,
626    pub sticky: bool,
627    pub unicode: bool,
628    /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
629    /// `g`/`y` flags. The newtype keeps it from being confused with the regex
630    /// engine's byte offsets, which are the same shape and differ off the BMP.
631    pub last_index: crate::utf16::U16Index,
632}
633
634/// A Promise's settled state and pending reactions.
635pub struct PromiseCell {
636    pub state: PromiseState,
637    pub value: Value,
638    /// Reactions registered while still pending; drained (as microtasks) on
639    /// settle.
640    pub reactions: Vec<PromiseReaction>,
641    /// True once a rejection has been observed by a handler (`.then`/`.catch`),
642    /// so the loop doesn't report it as unhandled.
643    pub handled: bool,
644}
645
646/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
647/// a native continuation (Promise chaining / async `await` resumption).
648pub enum PromiseReaction {
649    Js {
650        on_ful: Value,
651        on_rej: Value,
652        result: Value,
653    },
654    Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
655}
656
657#[derive(Default, Clone, Copy, PartialEq, Eq)]
658pub enum PromiseState {
659    #[default]
660    Pending,
661    Fulfilled,
662    Rejected,
663}
664
665/// A live class constructor. The prototype object (holding instance methods) and
666/// the static-side own properties live on the heap; `parent` is the superclass
667/// constructor value (`None` for a base class).
668#[derive(Clone)]
669pub struct ClassVal {
670    pub name: String,
671    /// The constructor function value (a `JsObj::Func`), or `None` for a class
672    /// with only a synthesized default constructor.
673    pub ctor: Option<Value>,
674    pub parent: Option<Value>,
675    /// `C.prototype` — the object instances delegate to.
676    pub proto: Value,
677    /// Static own properties (static methods/fields), plus `name`/`prototype`.
678    pub statics: IndexMap<String, Value>,
679    /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
680    /// per-instance after `super()` (or at construction start for a base class).
681    /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
682    /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
683    /// result — it cannot be re-derived at run time (a field initialised from an
684    /// already-anonymous function held elsewhere must not be renamed).
685    pub fields: Vec<(String, Value, bool)>,
686    /// The FuncDef holding the class's source span (`String(C)`).
687    pub source_def: Option<usize>,
688}
689
690/// The result of resolving `super.name`: a getter to invoke (accessor property)
691/// or a directly-usable value (method / data property).
692pub enum SuperRef {
693    Getter(Value),
694    Data(Value),
695}
696
697/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
698/// `+0` are the same key, primitives compare by value, objects by heap identity.
699#[derive(Clone, PartialEq, Eq, Hash)]
700pub enum MapKey {
701    Undef,
702    Null,
703    Bool(bool),
704    /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
705    Num(u64),
706    /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
707    Big(String),
708    Str(String),
709    /// Heap identity (objects, arrays, functions, symbols).
710    Ref(u32),
711    /// A builtin intrinsic, by the name it answers to. Every bare reference
712    /// to `Math` or `parseInt` allocates a fresh handle, so heap identity
713    /// would make `new Set([Math, Math])` two entries; `strict_eq` compares
714    /// these by name too.
715    Intrinsic(String),
716}
717
718// ── environments ─────────────────────────────────────────────────────────────
719
720/// The map behind a scope. Hashing these with `FxHash` instead of the default
721/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
722/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
723pub type VarMap = IndexMap<String, Value>;
724
725/// A local-variable environment, shared (by `Rc`) between a frame and any nested
726/// function that captures it.
727pub struct EnvData {
728    pub vars: VarMap,
729    /// The names in `vars` that were declared `const`, so an assignment to one
730    /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
731    ///
732    /// A separate set rather than a flag inside `VarMap`'s value, because
733    /// `set_name` is a hot path — the common case is an env with NO consts,
734    /// where `is_empty()` settles it without hashing the name a second time.
735    pub consts: rustc_hash::FxHashSet<String>,
736    pub parent: Option<Env>,
737}
738pub type Env = Rc<RefCell<EnvData>>;
739
740/// An accessor property: `(getter, setter)`, either optional.
741pub type Accessor = (Option<Value>, Option<Value>);
742
743/// Prefix of the hidden property-map entry that reserves an accessor's slot in
744/// own-key insertion order (see `set_accessor`).
745pub const ORD_MARKER: &str = "@@ord:";
746
747/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
748/// all-true shape a plain `o.k = v` assignment produces, which is why only
749/// deviations need storing.
750#[derive(Clone, Copy, Debug, PartialEq, Eq)]
751pub struct PropAttrs {
752    pub writable: bool,
753    pub enumerable: bool,
754    pub configurable: bool,
755}
756
757impl Default for PropAttrs {
758    fn default() -> Self {
759        PropAttrs {
760            writable: true,
761            enumerable: true,
762            configurable: true,
763        }
764    }
765}
766
767impl PropAttrs {
768    /// The attribute shape V8 gives an internal-but-inspectable slot such as
769    /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
770    /// readable and replaceable, but never enumerated.
771    pub const HIDDEN: PropAttrs = PropAttrs {
772        writable: true,
773        enumerable: false,
774        configurable: true,
775    };
776}
777
778fn new_env(parent: Option<Env>) -> Env {
779    Rc::new(RefCell::new(EnvData {
780        vars: VarMap::default(),
781        consts: rustc_hash::FxHashSet::default(),
782        parent,
783    }))
784}
785
786/// A fresh empty scope chained under `parent`.
787pub fn child_env(parent: Env) -> Env {
788    new_env(Some(parent))
789}
790
791/// One function activation.
792pub struct Frame {
793    pub env: Env,
794    /// The env this activation started in — the FUNCTION scope. `var` and hoisted
795    /// function declarations bind here no matter how many block scopes are open.
796    pub base_env: Env,
797    pub this_obj: Option<Value>,
798    /// `new.target` for this activation (the constructor when invoked via `new`).
799    pub new_target: Option<Value>,
800    /// The class value owning the running method (drives `super`); `None` outside
801    /// a class method/constructor.
802    pub home_class: Option<Value>,
803    /// Whether the running method is a static one — see `FuncVal::home_static`.
804    pub home_static: bool,
805    /// The object literal owning the running method — see
806    /// `FuncVal::home_object`.
807    pub home_object: Option<Value>,
808    /// Whether the code in this activation is strict. A write the object
809    /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
810    /// ASSIGNMENT SITE decides — not the object being written to.
811    pub strict: bool,
812    /// Source line the frame is currently executing (updated by the DAP line hook
813    /// under `--dap`; stays 0 on ordinary runs).
814    pub line: u32,
815    /// The function name that owns this frame, for the DAP `stackTrace`; `None`
816    /// for the module frame and anonymous activations.
817    pub owner: Option<String>,
818    /// True ONLY for the program's module frame. A generator/async body runs on a
819    /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
820    /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
821    /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
822    /// across concurrent activations of the same function.
823    pub is_module: bool,
824    /// Whether this activation's `this` is bound yet — see [`ThisState`].
825    pub this_state: ThisState,
826}
827
828/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
829/// is observable: only a DERIVED class constructor starts with `this`
830/// uninitialized, and only `super()` binds it.
831///
832/// The instance is still allocated up front (`construct_class`), so the
833/// state is what makes it unreachable until then: `this` before `super()`, a
834/// second `super()`, and returning without one are each the error node raises
835/// rather than a silent write to the pre-allocated object.
836#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
837pub enum ThisState {
838    /// Every other activation: `this` is whatever was passed in.
839    #[default]
840    Plain,
841    /// A derived constructor before `super()` has returned.
842    Pending,
843    /// A derived constructor after `super()`.
844    Bound,
845}
846
847/// A non-local control signal. `Break`/`Continue` carry the optional loop label
848/// and are only raised when the target loop lives in an ENCLOSING chunk (a
849/// `break` inside a `try` block, which the host runs as its own chunk); a
850/// same-chunk `break` is a plain compiler-resolved jump.
851#[derive(Clone)]
852pub enum Signal {
853    Return(Value),
854    Break(Option<String>),
855    Continue(Option<String>),
856}
857
858/// The JavaScript runtime.
859pub struct JsHost {
860    heap: Vec<JsObj>,
861    /// Function templates, indexed by def id.
862    pub funcs: Vec<FuncDef>,
863    /// Every script text a loaded program was parsed from; a `FuncDef`'s
864    /// `script` indexes it and its `span` slices it.
865    pub scripts: Vec<std::sync::Arc<str>>,
866    /// try/catch/finally block templates, indexed by try id.
867    pub tries: Vec<TryDef>,
868    /// Module-level (global) names.
869    globals: VarMap,
870    /// The one uninitialized-binding marker, allocated on first use. See
871    /// [`JsHost::tdz_marker`].
872    tdz: Option<Value>,
873    /// Module-top-level names still in their temporal dead zone. Kept out of
874    /// `globals` so the marker is never reachable as `globalThis.<name>`.
875    tdz_globals: rustc_hash::FxHashSet<String>,
876    /// Top-level `const` names (a module frame declares into `globals`), so an
877    /// assignment to one throws the same way a block-scoped `const` does.
878    global_consts: rustc_hash::FxHashSet<String>,
879    /// The frame stack (bottom = module).
880    frames: Vec<Frame>,
881    /// The program's top-level scope — the scope runtime-compiled source runs in
882    /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
883    /// `run_chunk_in_global_scope`), as opposed to whatever function frame
884    /// happens to be executing when that source is compiled.
885    ///
886    /// Held as its own field rather than read off `frames[0]` because a coroutine
887    /// body runs with `frames` SWAPPED for its own one-frame context
888    /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
889    ///
890    /// Note this is node-js's ONE top-level scope. Node distinguishes the global
891    /// scope from a CommonJS module's scope (a module body is a wrapper
892    /// function), so in Node a file's top-level `var` is invisible to dynamic
893    /// code; here the entry file is evaluated with Script semantics, so it stays
894    /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
895    /// records for top-level `return`, not a separate one — and `node -e`, which
896    /// really is a Script, matches Node exactly.
897    global_env: Env,
898    pub error: Option<String>,
899    /// The in-flight thrown value, if any (JS `throw`).
900    pub exc: Option<Value>,
901    pub signal: Option<Signal>,
902    /// Promises that settled REJECTED this tick. Drained at each microtask
903    /// checkpoint: any still without a handler is an unhandled rejection.
904    pub pending_rejections: Vec<u32>,
905    /// `process.on(event, fn)` listeners, by event name.
906    pub process_listeners: IndexMap<String, Vec<ProcListener>>,
907    /// The canonical `null` handle (allocated once).
908    null_val: Value,
909    /// `[[Prototype]]` link per heap object, by heap index. Absent = default
910    /// (`Object.prototype` for objects, `null` for the root).
911    protos: HashMap<u32, Value>,
912    /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
913    /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
914    /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
915    /// is why `Object.create(null) instanceof Object` can read `false`.
916    null_proto_objs: HashSet<u32>,
917    /// Own properties of function objects (functions are objects in JS): a live
918    /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
919    fn_props: HashMap<u32, IndexMap<String, Value>>,
920    /// Accessor (getter/setter) properties per owning object, by heap index then
921    /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
922    accessors: HashMap<u32, IndexMap<String, Accessor>>,
923    /// Own-property attributes that deviate from the plain-assignment default
924    /// (`{writable, enumerable, configurable}` all true), by heap index then key.
925    /// Only non-default entries are stored, so an ordinary object costs nothing;
926    /// `prop_attrs` returns the default for any key absent here. This is what
927    /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
928    /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
929    /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
930    prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
931    /// Heap objects sealed against new properties by `Object.preventExtensions`,
932    /// `Object.seal` or `Object.freeze`.
933    non_extensible: HashSet<u32>,
934    /// Private names (`#m`) declared as a METHOD or accessor rather than as a
935    /// field, for the brand-check error text: node distinguishes `Receiver must
936    /// be an instance of class C` (a private method or accessor) from `Cannot
937    /// read private member #x …` (a private field). Which class is answered by
938    /// the running method's home class, not by this set, so two classes
939    /// declaring the same private method name stay exact.
940    private_methods: HashSet<String>,
941    /// The ELIDED element positions of each array, by heap index. Absent (the
942    /// overwhelmingly common case) means the array is dense.
943    ///
944    /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
945    /// to be mapped back to `undefined` at every element read in the runtime, and
946    /// a single missed read would leak an un-nameable value into user code — a
947    /// worse failure than storing `undefined` and losing the distinction. Keeping
948    /// the marker OUTSIDE the value domain makes that leak structurally
949    /// impossible: the element vector still holds a perfectly ordinary
950    /// `Value::Undef` at a hole, so any code path that has not been taught about
951    /// holes degrades to exactly the pre-existing behaviour (a visible
952    /// `undefined`) instead of producing something unrepresentable.
953    ///
954    /// Sized like the array it describes in the worst case (`new Array(n)` marks
955    /// every index), which is the same order as the `Vec<Value>` already paid for
956    /// that array — so it cannot turn a working allocation into an OOM.
957    array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
958    /// See `take_super_replacement`.
959    super_replacement: Option<Value>,
960    /// Set by `run_class_ctor` for the one call that follows: the next user
961    /// function activation is a derived constructor and starts `Pending`.
962    derived_ctor_next: bool,
963    /// Whether the entry script's top-level `var`s bind to its own scope rather
964    /// than to the globals map — the CommonJS wrapper Node puts every file in.
965    module_scope: bool,
966    /// User-assigned static properties on a builtin namespace/constructor, keyed
967    /// by namespace name then property (`Error` → `prepareStackTrace`,
968    /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
969    /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
970    /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
971    builtin_statics: HashMap<String, IndexMap<String, Value>>,
972    /// The shared well-known `Object.prototype` object (chain root for objects).
973    object_proto: Value,
974    /// Class name of each class `prototype` object, by heap index — lets an
975    /// instance recover its constructor name (for `util.inspect` prefix and
976    /// `obj.constructor.name`).
977    proto_class: HashMap<u32, Value>,
978    /// Class constructor values by name, so a running method's `home_class` name
979    /// resolves to its class value (for `super`).
980    class_registry: HashMap<String, Value>,
981    /// Well-known prototype objects for the builtin error constructors, by name.
982    error_protos: HashMap<String, Value>,
983    /// The template object of each tagged-template SITE, keyed by the chunk that
984    /// holds the site and the site's ordinal within its compilation.
985    ///
986    /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
987    /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
988    /// `t() === t()` true, and a tag that memoizes on the strings array — the
989    /// documented reason the object is cached, and how `lit-html` and `graphql`
990    /// avoid re-parsing — saw a fresh array every call here. Two sites with
991    /// identical text are still distinct objects, which the chunk hash plus the
992    /// ordinal keep apart.
993    template_objects: HashMap<(u64, u64), Value>,
994    /// Real prototype *objects* for the builtin exotics whose instances need a
995    /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
996    /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
997    /// cannot appear on a prototype chain and report `typeof "function"`.
998    native_protos: HashMap<String, Value>,
999    /// `Symbol.for` registry: description → symbol value.
1000    symbol_registry: HashMap<String, Value>,
1001    /// Monotonic id source for fresh `Symbol()` values.
1002    next_symbol: u64,
1003    /// Every live symbol by its id, so a `@@sym:<id>` property key can be
1004    /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
1005    symbols_by_id: HashMap<u64, Value>,
1006    /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
1007    /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
1008    /// is a distinct key.
1009    well_known_ids: HashMap<u64, String>,
1010    /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
1011    generators: Vec<GenCell>,
1012    /// Promise cells, indexed by `JsObj::Promise.id`.
1013    promises: Vec<PromiseCell>,
1014    /// Whether the loop is part-way through draining the microtask queue, so a
1015    /// `nextTick` queued by one of them waits for the round to finish. See
1016    /// `next_microtask`.
1017    draining_micro: bool,
1018    /// `process.nextTick` callbacks (drained before promise microtasks).
1019    pub nextticks: std::collections::VecDeque<Task>,
1020    /// Promise-reaction / `queueMicrotask` microtasks.
1021    pub microtasks: std::collections::VecDeque<Task>,
1022    /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
1023    pub macrotasks: Vec<Timer>,
1024    /// Monotonic timer-id source.
1025    next_timer: u64,
1026    /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
1027    /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
1028    /// sees a spurious `Disconnected` while a server is running.
1029    io_tx: Sender<IoTask>,
1030    /// Owned by the event loop (taken out for the blocking `recv`). Receives the
1031    /// `IoTask`s posted by I/O threads.
1032    io_rx: Option<Receiver<IoTask>>,
1033    /// Ref-count of "things keeping the process alive": open listeners, live
1034    /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1035    /// queues are empty. A pure script never touches it, so it exits exactly as
1036    /// before.
1037    open_handles: usize,
1038    /// In-process output sink. When `Some`, everything the program writes to
1039    /// stdout/stderr is appended here instead of reaching the process streams —
1040    /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1041    /// cannot corrupt its display. `None` (the default) is the ordinary
1042    /// standalone `node` behaviour: writes go straight to the real streams.
1043    ///
1044    /// Bytes, not `String`: a program may legitimately write output that is not
1045    /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1046    /// buffer can only hold the lossy `U+FFFD` transcription of it.
1047    capture: Option<Vec<u8>>,
1048    /// `process.exitCode`: the code the process exits with when the event loop
1049    /// drains, or `None` while unset. Separate from an explicit
1050    /// `process.exit(n)`, which exits immediately with `n`.
1051    pub exit_code: Option<i32>,
1052    /// Whether the `exit` event has already been emitted, so the `process.exit`
1053    /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1054    pub exiting: bool,
1055    /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1056    /// identity in JS, so `globalThis === globalThis` is `true` and a property
1057    /// written through one read is visible through the next. Minting a fresh
1058    /// object per read made both false.
1059    global_obj: Value,
1060}
1061
1062/// One `process.on`/`process.once` registration. `once` is not decoration: a
1063/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1064/// the same event does not reach it. Treating `once` as an alias of `on` made
1065/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1066/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1067/// reports zero listeners afterwards.
1068#[derive(Clone)]
1069pub struct ProcListener {
1070    pub f: Value,
1071    pub once: bool,
1072}
1073
1074/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1075/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1076pub enum Task {
1077    Js { cb: Value, args: Vec<Value> },
1078    Native(Box<dyn FnOnce() -> Result<(), String>>),
1079}
1080
1081impl Task {
1082    fn run(self) -> Result<(), String> {
1083        match self {
1084            Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1085            Task::Native(f) => f(),
1086        }
1087    }
1088}
1089
1090/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1091/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1092pub struct Timer {
1093    pub id: u64,
1094    pub delay: f64,
1095    pub seq: u64,
1096    pub callback: Value,
1097    pub args: Vec<Value>,
1098    pub cancelled: bool,
1099    /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1100    /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1101    /// deadline each time it fires, so it keeps the loop alive indefinitely —
1102    /// exactly like Node, where `setInterval` runs until cleared.
1103    pub interval: Option<f64>,
1104    /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1105    /// the event loop alive; an unref'd one still fires while the loop happens
1106    /// to be alive for another reason, but never holds it open by itself.
1107    pub refed: bool,
1108    /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1109    /// path (an open handle or a pending interval). On the pure virtual clock
1110    /// this is ignored.
1111    pub deadline: Instant,
1112}
1113
1114/// One suspended generator. `coro` is `None` only while actively running (taken
1115/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1116/// (frames/signal/error/exc) while suspended.
1117struct GenCell {
1118    coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1119    /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1120    /// thread → valid for the body's life). Read by `yield` to suspend.
1121    yielder: *const (),
1122    ctx: GenContext,
1123    done: bool,
1124    /// True once the body has been resumed at least once (so it is suspended at a
1125    /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1126    started: bool,
1127    /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1128    /// `yield` resume so the body unwinds (running any pending `finally`).
1129    inject: Option<GenInject>,
1130    /// True for an `async function*` body, where `await` AND `yield` share one
1131    /// coroutine yielder: `await` wraps its operand in an await marker so the
1132    /// driver can tell an internal suspension from a real yield.
1133    async_gen: bool,
1134    /// `[[AsyncGeneratorQueue]]` — pending requests as
1135    /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1136    /// overlapping requests resume the body ONE AT A TIME and settle in request
1137    /// order; without it a second request issued before the first settles races
1138    /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1139    /// all enqueue — a `.return()` that skipped the queue would terminate the
1140    /// body while an earlier `.next()` was still suspended on an `await`, and
1141    /// that `.next()` would then wrongly report `{done: true}`.
1142    queue: std::collections::VecDeque<(GenReq, u32)>,
1143    /// True while a queued request is being driven.
1144    running: bool,
1145    /// The [`stack_floor`] that applies while this generator's body is running.
1146    ///
1147    /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1148    /// range the thread's pthread record describes says nothing about how much
1149    /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1150    /// construction and swapped in around every resume; without it the guard
1151    /// compared a coroutine stack pointer against the main stack's floor and
1152    /// (depending on where mmap landed) either fired immediately or never.
1153    stack_floor: usize,
1154}
1155
1156/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1157enum GenInject {
1158    Return(Value),
1159    Throw(Value),
1160}
1161
1162/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1163/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1164/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1165/// instead of unwinding the body on the spot.
1166#[derive(Clone)]
1167pub enum GenReq {
1168    /// `.next(v)` — resume normally with `v`.
1169    Next(Value),
1170    /// `.return(v)` — resume with a forced return completion.
1171    Return(Value),
1172    /// `.throw(e)` — resume with a forced throw completion.
1173    Throw(Value),
1174}
1175
1176/// The mutable "execution registers" swapped at every generator resume/suspend
1177/// boundary so a suspended generator's half-finished frame/signal state never
1178/// leaks into the resuming caller. The heap, function/class tables and globals
1179/// are shared and never swapped.
1180#[derive(Default)]
1181struct GenContext {
1182    frames: Vec<Frame>,
1183    error: Option<String>,
1184    exc: Option<Value>,
1185    signal: Option<Signal>,
1186}
1187
1188thread_local! {
1189    /// Id of the generator whose body is currently executing, or `None` at the
1190    /// root. `yield` suspends this generator.
1191    static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1192}
1193
1194thread_local! {
1195    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1196}
1197
1198/// Run `f` with mutable access to the thread-local host.
1199pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1200    HOST.with(|h| f(&mut h.borrow_mut()))
1201}
1202
1203/// Reset the host to a clean slate (fresh module frame).
1204pub fn reset_host() {
1205    with_host(|h| *h = JsHost::new());
1206    // Drop any cached module handles / factory closure — they index the old heap.
1207    crate::module::reset();
1208}
1209
1210impl Default for JsHost {
1211    fn default() -> Self {
1212        Self::new()
1213    }
1214}
1215
1216impl JsHost {
1217    pub fn new() -> JsHost {
1218        let global_env = new_env(None);
1219        let (io_tx, io_rx) = std::sync::mpsc::channel();
1220        let mut h = JsHost {
1221            tdz: None,
1222            tdz_globals: Default::default(),
1223            heap: Vec::new(),
1224            funcs: Vec::new(),
1225            scripts: Vec::new(),
1226            tries: Vec::new(),
1227            globals: VarMap::default(),
1228            global_consts: rustc_hash::FxHashSet::default(),
1229            frames: vec![Frame {
1230                env: global_env.clone(),
1231                base_env: global_env.clone(),
1232                this_obj: None,
1233                new_target: None,
1234                home_class: None,
1235                home_static: false,
1236                home_object: None,
1237                strict: false,
1238                line: 0,
1239                owner: None,
1240                is_module: true,
1241                this_state: ThisState::Plain,
1242            }],
1243            global_env,
1244            error: None,
1245            exc: None,
1246            signal: None,
1247            pending_rejections: Vec::new(),
1248            process_listeners: IndexMap::new(),
1249            null_val: Value::Undef,
1250            protos: HashMap::new(),
1251            null_proto_objs: HashSet::new(),
1252            fn_props: HashMap::new(),
1253            accessors: HashMap::new(),
1254            prop_attrs: HashMap::new(),
1255            non_extensible: HashSet::new(),
1256            private_methods: HashSet::new(),
1257            array_holes: HashMap::new(),
1258            super_replacement: None,
1259            derived_ctor_next: false,
1260            module_scope: false,
1261            builtin_statics: HashMap::new(),
1262            object_proto: Value::Undef,
1263            proto_class: HashMap::new(),
1264            class_registry: HashMap::new(),
1265            error_protos: HashMap::new(),
1266            template_objects: HashMap::new(),
1267            native_protos: HashMap::new(),
1268            symbol_registry: HashMap::new(),
1269            next_symbol: 1,
1270            symbols_by_id: HashMap::new(),
1271            well_known_ids: HashMap::new(),
1272            generators: Vec::new(),
1273            promises: Vec::new(),
1274            microtasks: std::collections::VecDeque::new(),
1275            draining_micro: false,
1276            nextticks: std::collections::VecDeque::new(),
1277            macrotasks: Vec::new(),
1278            next_timer: 1,
1279            io_tx,
1280            io_rx: Some(io_rx),
1281            open_handles: 0,
1282            capture: None,
1283            exit_code: None,
1284            exiting: false,
1285            global_obj: Value::Undef,
1286        };
1287        h.null_val = h.alloc(JsObj::Null);
1288        // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1289        h.object_proto = h.new_object(IndexMap::new());
1290        h.global_obj = h.new_object(IndexMap::new());
1291        h
1292    }
1293
1294    /// Whether `v` IS the one `globalThis` object (not merely an object).
1295    pub fn is_global_object(&self, v: &Value) -> bool {
1296        !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1297    }
1298
1299    /// The `globalThis` object — one per host, so its identity and its
1300    /// properties both survive across reads.
1301    pub fn global_object(&mut self) -> Value {
1302        if matches!(self.global_obj, Value::Undef) {
1303            self.global_obj = self.new_object(IndexMap::new());
1304        }
1305        self.global_obj.clone()
1306    }
1307
1308    // ── prototype chain ──────────────────────────────────────────────────
1309    /// The `[[Prototype]]` of a heap value, if explicitly linked.
1310    pub fn proto_of(&self, v: &Value) -> Option<Value> {
1311        if let Value::Obj(i) = v {
1312            self.protos.get(i).cloned()
1313        } else {
1314            None
1315        }
1316    }
1317    /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1318    /// null-prototype object (recorded so `instanceof Object` reads false);
1319    /// undefined just clears any link without the null marker.
1320    pub fn set_proto(&mut self, v: &Value, proto: Value) {
1321        if let Value::Obj(i) = v {
1322            if self.is_null(&proto) {
1323                self.protos.remove(i);
1324                self.null_proto_objs.insert(*i);
1325            } else if matches!(proto, Value::Undef) {
1326                self.protos.remove(i);
1327            } else {
1328                self.protos.insert(*i, proto);
1329                self.null_proto_objs.remove(i);
1330            }
1331        }
1332    }
1333    /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1334    pub fn has_null_proto(&self, v: &Value) -> bool {
1335        matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1336    }
1337    /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1338    /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1339    /// for `Object.prototype` is null even though nothing ever set it so: it is
1340    /// the chain root and was never passed through `set_proto`, so the
1341    /// explicitly-nulled registry does not hold it and `console.log(Object
1342    /// .prototype)` printed a bare `{}` where node prints the tag.
1343    ///
1344    /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1345    /// about whether Object.prototype's own methods and `__proto__` accessor are
1346    /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1347    pub fn inspects_null_proto(&self, v: &Value) -> bool {
1348        self.has_null_proto(v) || *v == self.object_proto
1349    }
1350    pub fn object_proto(&self) -> Value {
1351        self.object_proto.clone()
1352    }
1353    /// Record that the prototype object `proto` belongs to the class constructor
1354    /// `class_val` (so instances can recover their constructor).
1355    pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1356        if let Value::Obj(i) = proto {
1357            self.proto_class.insert(*i, class_val);
1358        }
1359    }
1360    /// The class whose `prototype` object IS `v`, if `v` is one.
1361    pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1362        match v {
1363            Value::Obj(i) => self.proto_class.get(i).cloned(),
1364            _ => None,
1365        }
1366    }
1367    /// The class constructor value nearest in `obj`'s prototype chain, if any.
1368    pub fn class_of(&self, obj: &Value) -> Option<Value> {
1369        let mut cur = self.proto_of(obj);
1370        while let Some(p) = cur {
1371            if let Value::Obj(i) = &p {
1372                if let Some(c) = self.proto_class.get(i) {
1373                    return Some(c.clone());
1374                }
1375            }
1376            cur = self.proto_of(&p);
1377        }
1378        None
1379    }
1380    /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1381    /// object, no prefix).
1382    pub fn ctor_name(&self, obj: &Value) -> String {
1383        if let Some(c) = self.class_of(obj) {
1384            if let Some(JsObj::Class(cv)) = self.get(&c) {
1385                return cv.name.clone();
1386            }
1387        }
1388        // A `function F(){}` constructor is not a `class`, so it has no
1389        // `proto_class` entry. V8's `getConstructorName` walks the prototype
1390        // chain for an own `constructor` that is a named function — which is
1391        // what makes `console.log(new F())` print `F { y: 2 }`.
1392        let mut cur = self.proto_of(obj);
1393        while let Some(p) = cur {
1394            let ctor = match self.get(&p) {
1395                Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1396                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1397                _ => None,
1398            };
1399            if let Some(f) = ctor {
1400                let n = self.callable_name(&f);
1401                if !n.is_empty() {
1402                    return n;
1403                }
1404            }
1405            cur = self.proto_of(&p);
1406        }
1407        String::new()
1408    }
1409
1410    /// Whether a callable owns a `prototype` property. `MakeConstructor`
1411    /// (10.2.5) runs for an ordinary function definition and for every
1412    /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1413    /// function are not constructors and own none.
1414    pub fn owns_prototype(&self, v: &Value) -> bool {
1415        match self.get(v) {
1416            Some(JsObj::Class(_)) => true,
1417            Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1418                Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1419                None => false,
1420            },
1421            _ => false,
1422        }
1423    }
1424
1425    /// A function's own-property table (created on demand).
1426    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1427        if let Value::Obj(i) = v {
1428            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1429        } else {
1430            None
1431        }
1432    }
1433
1434    /// A class static member, inherited down the constructor chain: a subclass
1435    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1436    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1437        let mut cur = class_val.clone();
1438        loop {
1439            if let Some(v) = self.fn_prop(&cur, name) {
1440                return Some(v);
1441            }
1442            match self.get(&cur) {
1443                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1444                _ => return None,
1445            }
1446        }
1447    }
1448
1449    /// The first `extends` ancestor that is NOT a user class — the builtin
1450    /// constructor a class chain bottoms out in (`class D extends Array {}` →
1451    /// the `Array` builtin), or `None` for a chain of user classes only.
1452    ///
1453    /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1454    /// stops being a `Class`, so a static declared by the BUILTIN half of the
1455    /// chain was unreachable: `D.from` read `undefined` where node inherits
1456    /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1457    /// with an ordinary property read, which is what reaches a builtin's
1458    /// statics.
1459    pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1460        let mut cur = class_val.clone();
1461        loop {
1462            match self.get(&cur) {
1463                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1464                _ => return Some(cur),
1465            }
1466        }
1467    }
1468    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1469        if let Value::Obj(i) = v {
1470            self.fn_props
1471                .entry(*i)
1472                .or_default()
1473                .insert(name.to_string(), val);
1474        }
1475        // `name` and `prototype` are own properties of every function/class, but
1476        // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1477        // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1478        // script assigned. An ARRAY receiver reaching the same side table has no
1479        // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1480        if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1481            return;
1482        }
1483        let attrs = match name {
1484            "name" => PropAttrs {
1485                writable: false,
1486                enumerable: false,
1487                configurable: true,
1488            },
1489            "prototype" => PropAttrs {
1490                writable: true,
1491                enumerable: false,
1492                configurable: false,
1493            },
1494            _ => return,
1495        };
1496        self.set_prop_attrs(v, name, attrs);
1497    }
1498    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1499    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1500        self.builtin_statics
1501            .get(ns)
1502            .and_then(|m| m.get(name).cloned())
1503    }
1504    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1505    /// handles for the same namespace).
1506    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1507        self.builtin_statics
1508            .entry(ns.to_string())
1509            .or_default()
1510            .insert(name.to_string(), val);
1511    }
1512    /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1513    /// key was there — without this, `delete Array.prototype.patch` answered
1514    /// true and left the entry in place, so the patch outlived its own removal.
1515    pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1516        self.builtin_statics
1517            .get_mut(ns)
1518            .is_some_and(|m| m.shift_remove(name).is_some())
1519    }
1520    /// Every namespace a script has assigned a static onto, with that
1521    /// namespace's assigned keys — the source of the user-added half of
1522    /// `Object.getOwnPropertyNames(Array.prototype)`.
1523    pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1524        self.builtin_statics
1525            .get(ns)
1526            .map(|m| m.keys().cloned().collect())
1527            .unwrap_or_default()
1528    }
1529    /// Drop an own property from the side table (`delete arr.foo`,
1530    /// `delete fn.tag`). Reports whether the key was there.
1531    pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1532        match v {
1533            Value::Obj(i) => self
1534                .fn_props
1535                .get_mut(i)
1536                .map(|m| m.shift_remove(name).is_some())
1537                .unwrap_or(false),
1538            _ => false,
1539        }
1540    }
1541    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1542        if let Value::Obj(i) = v {
1543            self.fn_props
1544                .get(i)
1545                .map(|m| m.keys().cloned().collect())
1546                .unwrap_or_default()
1547        } else {
1548            Vec::new()
1549        }
1550    }
1551
1552    /// Install an accessor `(get, set)` for `key` on the object `owner`.
1553    pub fn set_accessor(
1554        &mut self,
1555        owner: &Value,
1556        key: &str,
1557        get: Option<Value>,
1558        set: Option<Value>,
1559    ) {
1560        if let Value::Obj(i) = owner {
1561            // Accessors live in their own table, but JS reports own keys in a
1562            // single insertion order across data AND accessor properties. Drop an
1563            // ordering marker into the property map so
1564            // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1565            // The marker is `@@`-prefixed, so it is invisible to every reader.
1566            let marker = format!("{ORD_MARKER}{key}");
1567            match self.get_mut(owner) {
1568                Some(JsObj::Object(props)) => {
1569                    if !props.contains_key(key) && !props.contains_key(&marker) {
1570                        props.insert(marker, Value::Undef);
1571                    }
1572                }
1573                // A function or class keeps its own properties in the fn-prop
1574                // side table, so its ordering marker belongs there. Without it a
1575                // static accessor enumerated AFTER every static field and method
1576                // regardless of where the class body declared it: node reports
1577                // `class A { static s = 2; static get sv(){} static m(){} }` as
1578                // `['sv', 'm', 's']` — the methods and accessors in source order
1579                // first, then the fields — and this reported `['m', 's', 'sv']`.
1580                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1581                    let table = self.fn_props.entry(*i).or_default();
1582                    if !table.contains_key(key) && !table.contains_key(&marker) {
1583                        table.insert(marker, Value::Undef);
1584                    }
1585                }
1586                _ => {}
1587            }
1588            let slot = self
1589                .accessors
1590                .entry(*i)
1591                .or_default()
1592                .entry(key.to_string())
1593                .or_insert((None, None));
1594            if get.is_some() {
1595                slot.0 = get;
1596            }
1597            if set.is_some() {
1598                slot.1 = set;
1599            }
1600        }
1601    }
1602    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1603    /// Drop an own accessor property entirely, marker and all.
1604    ///
1605    /// `delete obj.accessorProp` used to clear only the property map, and an
1606    /// accessor does not live there — so the delete reported success while the
1607    /// getter kept answering and `in` kept reporting the key.
1608    pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1609        if let Value::Obj(i) = owner {
1610            if let Some(m) = self.accessors.get_mut(i) {
1611                m.shift_remove(key);
1612            }
1613        }
1614        let marker = format!("{ORD_MARKER}{key}");
1615        match self.get_mut(owner) {
1616            Some(JsObj::Object(props)) => {
1617                props.shift_remove(&marker);
1618            }
1619            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1620                if let Value::Obj(i) = owner {
1621                    if let Some(t) = self.fn_props.get_mut(i) {
1622                        t.shift_remove(&marker);
1623                    }
1624                }
1625            }
1626            _ => {}
1627        }
1628    }
1629
1630    /// Turn an own accessor property into a data property carrying `value`,
1631    /// keeping its place in the own-key order.
1632    ///
1633    /// `set_accessor` records that order with an `@@ord:` marker in the
1634    /// property map rather than a real key, so deleting the accessor and
1635    /// inserting the value would append the key at the end instead. Node
1636    /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1637    /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1638    pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1639        if let Value::Obj(i) = owner {
1640            if let Some(m) = self.accessors.get_mut(i) {
1641                m.shift_remove(key);
1642            }
1643        }
1644        let marker = format!("{ORD_MARKER}{key}");
1645        let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1646            Some(pos) => {
1647                *map = map
1648                    .iter()
1649                    .enumerate()
1650                    .map(|(n, (k, v))| {
1651                        if n == pos {
1652                            (key.to_string(), value.clone())
1653                        } else {
1654                            (k.clone(), v.clone())
1655                        }
1656                    })
1657                    .collect();
1658            }
1659            None => {
1660                map.insert(key.to_string(), value.clone());
1661            }
1662        };
1663        let fn_table = matches!(
1664            self.get(owner),
1665            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1666        );
1667        if fn_table {
1668            if let Value::Obj(i) = owner {
1669                swap(self.fn_props.entry(*i).or_default());
1670            }
1671        } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1672            swap(props);
1673        }
1674    }
1675
1676    /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1677    ///
1678    /// Used when one object becomes another in place (a class extending a
1679    /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1680    /// already points at the leaf class's prototype, which is the one its
1681    /// methods must resolve through.
1682    pub fn move_index_state(&mut self, src: u32, dst: u32) {
1683        if let Some(holes) = self.array_holes.remove(&src) {
1684            self.array_holes.insert(dst, holes);
1685        }
1686        if let Some(attrs) = self.prop_attrs.remove(&src) {
1687            self.prop_attrs.entry(dst).or_default().extend(attrs);
1688        }
1689        if let Some(props) = self.fn_props.remove(&src) {
1690            self.fn_props.entry(dst).or_default().extend(props);
1691        }
1692        if let Some(acc) = self.accessors.remove(&src) {
1693            self.accessors.entry(dst).or_default().extend(acc);
1694        }
1695    }
1696
1697    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1698        if let Value::Obj(i) = owner {
1699            self.accessors.get(i).and_then(|m| m.get(key).cloned())
1700        } else {
1701            None
1702        }
1703    }
1704
1705    /// The own accessor-property keys of `owner`, in installation order.
1706    pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1707        match owner {
1708            Value::Obj(i) => self
1709                .accessors
1710                .get(i)
1711                .map(|m| m.keys().cloned().collect())
1712                .unwrap_or_default(),
1713            _ => Vec::new(),
1714        }
1715    }
1716
1717    // ── own-property attributes ──────────────────────────────────────────
1718
1719    /// Record non-default attributes for `owner[key]`. Storing the default shape
1720    /// clears the entry so the table only ever holds deviations.
1721    pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1722        if let Value::Obj(i) = owner {
1723            if attrs == PropAttrs::default() {
1724                if let Some(m) = self.prop_attrs.get_mut(i) {
1725                    m.shift_remove(key);
1726                }
1727            } else {
1728                self.prop_attrs
1729                    .entry(*i)
1730                    .or_default()
1731                    .insert(key.to_string(), attrs);
1732            }
1733        }
1734    }
1735
1736    /// Copy every recorded property attribute from `from` to `to`. A pass that
1737    /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1738    /// across or the copy silently re-exposes non-enumerable slots.
1739    pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1740        if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1741            if let Some(m) = self.prop_attrs.get(f).cloned() {
1742                for (k, a) in m {
1743                    self.set_prop_attrs(to, &k, a);
1744                }
1745            }
1746        }
1747    }
1748
1749    /// The attributes of own property `owner[key]` (all-true when unrecorded).
1750    pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1751        // An array's `length` is the array exotic's own property (10.4.2):
1752        // never enumerated and never configurable, and writable until
1753        // `Object.freeze` clears that — which is what stops a `push` from
1754        // extending a frozen array. Reporting it unconditionally writable made
1755        // `Object.isFrozen(Object.freeze([]))` false once the elements started
1756        // being sealed, because `length` was then the one key that never
1757        // followed.
1758        if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1759            let writable = match owner {
1760                Value::Obj(i) => self
1761                    .prop_attrs
1762                    .get(i)
1763                    .and_then(|m| m.get(key))
1764                    .map(|a| a.writable)
1765                    .unwrap_or(true),
1766                _ => true,
1767            };
1768            return PropAttrs {
1769                writable,
1770                enumerable: false,
1771                // An ARGUMENTS object's `length` is an ordinary data property
1772                // (10.4.4.6), so it is configurable where a real array's is
1773                // not. The two share a backing representation here, so the
1774                // exotic's attributes have to be told apart explicitly.
1775                configurable: crate::builtins::is_arguments_h(self, owner),
1776            };
1777        }
1778        match owner {
1779            Value::Obj(i) => self
1780                .prop_attrs
1781                .get(i)
1782                .and_then(|m| m.get(key))
1783                .copied()
1784                .unwrap_or_default(),
1785            _ => PropAttrs::default(),
1786        }
1787    }
1788
1789    /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1790    /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1791    pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1792        !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1793    }
1794
1795    /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1796    /// shape of every V8 "hidden but real" own property.
1797    pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1798        self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1799    }
1800
1801    /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1802    /// non-writable data property silently ignores the write in sloppy mode,
1803    /// which is the mode every script here runs in; so does adding a *new* key to
1804    /// a non-extensible object.
1805    pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1806        if !self.prop_attrs(owner, key).writable {
1807            return false;
1808        }
1809        // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1810        // and those members own no map entry for the walk below to find:
1811        // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1812        // is refused in node and was creating an own property here, which then
1813        // changed the object's brand.
1814        // The receiver's OWN kind counts too, not only the prototypes an
1815        // explicit link reaches: a plain array inherits `Array.prototype`
1816        // implicitly, with no link for the walk to follow, and
1817        // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1818        //
1819        // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1820        // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1821        // — are also OWN properties of every instance, so the inherited rule
1822        // never decides them; applying it anyway blocked `SetFunctionName`
1823        // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1824        // (x) {…}})` then threw.
1825        if key.starts_with("@@")
1826            && crate::builtins::own_ctor_name(self, owner)
1827                .into_iter()
1828                .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1829                .any(|c| crate::builtins::is_proto_readonly(c, key))
1830        {
1831            return false;
1832        }
1833        // 10.1.9.2: with no OWN property, the inherited one decides. A
1834        // non-writable data property up the chain blocks the write rather than
1835        // being shadowed — including one on a frozen prototype. Only own
1836        // attributes were consulted, so `Object.create(frozenBase).f = 2`
1837        // quietly created an own property node refuses to create.
1838        //
1839        // An inherited ACCESSOR does not block: its setter runs, and the write
1840        // path checks for one before reaching here.
1841        let has_own = match self.get(owner) {
1842            Some(JsObj::Object(p)) => p.contains_key(key),
1843            _ => true,
1844        };
1845        if !has_own {
1846            let mut cur = self.proto_of(owner);
1847            while let Some(proto) = cur {
1848                if self.own_accessor(&proto, key).is_some() {
1849                    break;
1850                }
1851                let present =
1852                    matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1853                if present {
1854                    if !self.prop_attrs(&proto, key).writable {
1855                        return false;
1856                    }
1857                    break;
1858                }
1859                cur = self.proto_of(&proto);
1860            }
1861        }
1862        if self.is_extensible(owner) {
1863            return true;
1864        }
1865        // A non-extensible object refuses a NEW key. Only the plain-object arm
1866        // could name its own keys, so every other shape answered "own" for any
1867        // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1868        // to the frozen template object a tagged template hands its tag.
1869        match self.get(owner) {
1870            Some(JsObj::Object(p)) => p.contains_key(key),
1871            Some(JsObj::Array(items)) => {
1872                key == "length"
1873                    || key
1874                        .parse::<usize>()
1875                        .map(|i| i < items.len())
1876                        .unwrap_or(false)
1877                    || self.fn_prop(owner, key).is_some()
1878            }
1879            // A RegExp's `lastIndex` is an own property, so a merely
1880            // NON-EXTENSIBLE regexp still accepts a write to it.
1881            Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1882            // Every other shape keeps its own properties in the fn-prop side
1883            // table (a function's statics, a Map's assigned properties), so
1884            // "does it already own this key" is that table's question. Answering
1885            // a blanket `true` let a NEW key land on a frozen function and a
1886            // frozen Map.
1887            _ => self.fn_prop(owner, key).is_some(),
1888        }
1889    }
1890
1891    /// Mark `v` closed to new properties (`Object.preventExtensions`).
1892    pub fn prevent_extensions(&mut self, v: &Value) {
1893        if let Value::Obj(i) = v {
1894            self.non_extensible.insert(*i);
1895        }
1896    }
1897
1898    pub fn is_extensible(&self, v: &Value) -> bool {
1899        !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1900    }
1901
1902    /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1903    /// the object and strip `configurable` — and, when freezing, `writable` —
1904    /// from every own property, data and accessor alike.
1905    pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1906        self.prevent_extensions(v);
1907        let mut keys = self.integrity_keys(v);
1908        keys.extend(self.own_accessor_keys(v));
1909        for k in keys {
1910            let mut a = self.prop_attrs(v, &k);
1911            a.configurable = false;
1912            if freeze {
1913                a.writable = false;
1914            }
1915            self.set_prop_attrs(v, &k, a);
1916        }
1917    }
1918
1919    /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1920    ///
1921    /// An array's elements are own properties too, and only the `Object` arm was
1922    /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1923    /// through, and the elements still reported `writable: true,
1924    /// configurable: true` while `Object.isFrozen` answered true over an empty
1925    /// key list. `length` is an own property as well, and freezing it is what
1926    /// stops a `push` from extending a frozen array.
1927    fn integrity_keys(&self, v: &Value) -> Vec<String> {
1928        let side_table_keys = |v: &Value| -> Vec<String> {
1929            match v {
1930                Value::Obj(i) => self
1931                    .fn_props
1932                    .get(i)
1933                    .map(|m| m.keys().cloned().collect())
1934                    .unwrap_or_default(),
1935                _ => Vec::new(),
1936            }
1937        };
1938        match self.get(v) {
1939            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1940            // A RegExp's only own property is its `lastIndex` cursor, which
1941            // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1942            // sealed nothing and a frozen regexp's cursor still moved.
1943            Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1944            // A function's statics and a Map's assigned properties live in the
1945            // fn-prop side table, and freezing has to reach them too.
1946            Some(JsObj::Func(_))
1947            | Some(JsObj::Class(_))
1948            | Some(JsObj::Map { .. })
1949            | Some(JsObj::Set { .. })
1950            | Some(JsObj::Promise { .. }) => side_table_keys(v),
1951            Some(JsObj::Array(items)) => (0..items.len())
1952                .map(|i| i.to_string())
1953                .chain(std::iter::once("length".to_string()))
1954                // A named property stuck on an array (`a.tag = 't'`, a match
1955                // array's `.index`/`.groups`) is an own property too, and
1956                // freezing has to reach it.
1957                .chain(match v {
1958                    Value::Obj(i) => self
1959                        .fn_props
1960                        .get(i)
1961                        .map(|m| m.keys().cloned().collect::<Vec<_>>())
1962                        .unwrap_or_default(),
1963                    _ => Vec::new(),
1964                })
1965                .collect(),
1966            _ => Vec::new(),
1967        }
1968    }
1969
1970    /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1971    pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1972        if self.is_extensible(v) {
1973            return false;
1974        }
1975        let mut keys = self.integrity_keys(v);
1976        keys.extend(self.own_accessor_keys(v));
1977        keys.iter().all(|k| {
1978            let a = self.prop_attrs(v, k);
1979            !a.configurable && (!freeze || !a.writable)
1980        })
1981    }
1982
1983    /// A fresh unique `Symbol(desc)` value.
1984    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1985        let id = self.next_symbol;
1986        self.next_symbol += 1;
1987        let v = self.alloc(JsObj::Symbol { desc, id });
1988        self.symbols_by_id.insert(id, v.clone());
1989        v
1990    }
1991
1992    /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1993    /// well-known `@@iterator`) came from.
1994    pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1995        if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1996            return self.symbols_by_id.get(&id).cloned();
1997        }
1998        let name = k.strip_prefix("@@")?;
1999        WELL_KNOWN_SYMBOLS
2000            .contains(&name)
2001            .then(|| {
2002                self.symbol_registry
2003                    .get(&format!("@@Symbol.{name}"))
2004                    .cloned()
2005            })
2006            .flatten()
2007    }
2008
2009    /// The own symbol-keyed property keys of `v` as SYMBOL values —
2010    /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
2011    pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
2012        let keys: Vec<String> = match self.get(v) {
2013            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
2014            // An Array/Function receiver has no property map: its non-index own
2015            // properties — symbol-keyed ones included — live in the fn-prop side
2016            // table, and are just as much own properties as an object's.
2017            Some(_) => self.fn_prop_keys(v),
2018            None => return Vec::new(),
2019        };
2020        keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2021    }
2022
2023    /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2024    /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2025    /// alongside the string keys, and what `Object.keys` / `for-in` /
2026    /// `JSON.stringify` deliberately skip.
2027    pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2028        match self.get(v) {
2029            Some(JsObj::Object(p)) => p
2030                .iter()
2031                .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2032                .map(|(k, val)| (k.clone(), val.clone()))
2033                .collect(),
2034            // Array/Function: the side table (see `own_symbol_keys`).
2035            Some(_) => self
2036                .fn_prop_keys(v)
2037                .into_iter()
2038                .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2039                .map(|k| {
2040                    let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2041                    (k, val)
2042                })
2043                .collect(),
2044            None => Vec::new(),
2045        }
2046    }
2047    /// The shared `Symbol.for(key)` value (interned by description).
2048    pub fn symbol_for(&mut self, key: &str) -> Value {
2049        if let Some(v) = self.symbol_registry.get(key) {
2050            return v.clone();
2051        }
2052        let s = self.new_symbol(Some(key.to_string()));
2053        self.symbol_registry.insert(key.to_string(), s.clone());
2054        s
2055    }
2056    /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2057    /// or `undefined` for a symbol that is not in the registry at all.
2058    ///
2059    /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2060    /// `Symbol('k')` share a description and only the first is registered. The
2061    /// `@@Symbol.*` well-known entries are registry-internal and never a
2062    /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2063    /// `undefined` there.
2064    pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2065        let Some(key) = self
2066            .symbol_registry
2067            .iter()
2068            .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2069            .map(|(k, _)| k.clone())
2070        else {
2071            return Value::Undef;
2072        };
2073        self.new_str(key)
2074    }
2075    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2076    /// property key is `@@iterator`).
2077    pub fn well_known_iterator(&mut self) -> Value {
2078        self.symbol_for("@@Symbol.iterator")
2079    }
2080    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2081    pub fn well_known_async_iterator(&mut self) -> Value {
2082        self.symbol_for("@@Symbol.asyncIterator")
2083    }
2084    /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2085    /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2086    /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2087    ///
2088    /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2089    /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2090    /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2091    /// different symbol, and identification is by id, so a user-made
2092    /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2093    pub fn well_known_symbol(&mut self, name: &str) -> Value {
2094        let key = format!("@@Symbol.{name}");
2095        if let Some(v) = self.symbol_registry.get(&key) {
2096            return v.clone();
2097        }
2098        let s = self.new_symbol(Some(format!("Symbol.{name}")));
2099        if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2100            self.well_known_ids.insert(*id, name.to_string());
2101        }
2102        self.symbol_registry.insert(key, s.clone());
2103        s
2104    }
2105    /// The internal property-key string for a value used as a key. A `Symbol`
2106    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2107    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2108    pub fn property_key(&self, v: &Value) -> String {
2109        if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2110            if let Some(n) = self.well_known_ids.get(id) {
2111                return format!("@@{n}");
2112            }
2113            return format!("@@sym:{id}");
2114        }
2115        self.str_of(v)
2116    }
2117
2118    pub fn null(&self) -> Value {
2119        self.null_val.clone()
2120    }
2121    pub fn is_null(&self, v: &Value) -> bool {
2122        matches!(self.get(v), Some(JsObj::Null))
2123    }
2124
2125    // ── program loading ──────────────────────────────────────────────────
2126    pub fn program_offsets(&self) -> (usize, usize) {
2127        (self.funcs.len(), self.tries.len())
2128    }
2129    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2130        self.funcs.extend(funcs);
2131        self.tries.extend(tries);
2132    }
2133    /// The source text of function `def_id`, when its program kept one.
2134    pub fn func_source(&self, def_id: usize) -> Option<&str> {
2135        let d = self.funcs.get(def_id)?;
2136        let (start, end) = d.span;
2137        if end == 0 {
2138            return None;
2139        }
2140        self.scripts
2141            .get(d.script? as usize)?
2142            .get(start as usize..end as usize)
2143    }
2144    pub fn try_def(&self, id: usize) -> Option<TryDef> {
2145        self.tries.get(id).cloned()
2146    }
2147
2148    /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2149    /// finalizer)` — without copying its chunks. Running a `try` used to clone
2150    /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2151    /// handler and its finalizer on every iteration just to learn its shape.
2152    pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2153        let t = self.tries.get(id)?;
2154        Some((
2155            t.handler.is_some(),
2156            t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2157            t.finalizer.is_some(),
2158        ))
2159    }
2160
2161    /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2162    /// Reached only when no pooled VM already holds that chunk.
2163    pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2164        let t = self.tries.get(id)?;
2165        match part {
2166            0 => Some(t.block.clone()),
2167            1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2168            _ => t.finalizer.clone(),
2169        }
2170    }
2171
2172    // ── heap allocation / accessors ──────────────────────────────────────
2173    pub fn alloc(&mut self, obj: JsObj) -> Value {
2174        self.heap.push(obj);
2175        Value::Obj((self.heap.len() - 1) as u32)
2176    }
2177    pub fn get(&self, v: &Value) -> Option<&JsObj> {
2178        if let Value::Obj(i) = v {
2179            self.heap.get(*i as usize)
2180        } else {
2181            None
2182        }
2183    }
2184    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2185        if let Value::Obj(i) = v {
2186            self.heap.get_mut(*i as usize)
2187        } else {
2188            None
2189        }
2190    }
2191    /// Which variant `v` points at, without copying its contents. Use this in
2192    /// place of `get(v).cloned()` whenever only the tag is needed — see
2193    /// [`ObjKind`].
2194    pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2195        self.get(v).map(JsObj::kind)
2196    }
2197    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2198        self.alloc(JsObj::Str(s.into()))
2199    }
2200    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2201        self.alloc(JsObj::Array(items))
2202    }
2203
2204    /// Record that `name` was declared as a private method or accessor.
2205    pub fn note_private_method(&mut self, name: &str) {
2206        self.private_methods.insert(name.to_string());
2207    }
2208
2209    /// Whether `name` was declared as a private method/accessor by some class,
2210    /// as opposed to a private field.
2211    pub fn is_private_method(&self, name: &str) -> bool {
2212        self.private_methods.contains(name)
2213    }
2214
2215    /// The name of the class whose body the running function belongs to. Only a
2216    /// method of that class can even mention its private names, so this is the
2217    /// class a failed brand check must name.
2218    /// The `super` binding of the frame now running: the owning class name,
2219    /// whether the method is static, and the home object of an object-literal
2220    /// method. An ARROW captures all three at creation, the way it captures
2221    /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2222    /// Whether the activation now running is strict code.
2223    /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2224    /// with no `'use strict'` of its own or inherited from its script. This is
2225    /// the receiver test the `arguments`/`caller` poison pill keys on: node
2226    /// decides by the FUNCTION, never by the code doing the reading.
2227    pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2228        match self.get(v) {
2229            Some(JsObj::Func(fv)) => {
2230                !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2231            }
2232            _ => false,
2233        }
2234    }
2235    pub fn current_strict(&self) -> bool {
2236        self.frame().strict
2237    }
2238
2239    /// Mark the frame about to run as STRICT — used for a program whose own top
2240    /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2241    pub fn set_current_strict(&mut self) {
2242        if let Some(f) = self.frames.last_mut() {
2243            f.strict = true;
2244        }
2245    }
2246
2247    pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2248        (
2249            self.current_home_class_name(),
2250            self.frame().home_static,
2251            self.frame().home_object.clone(),
2252        )
2253    }
2254
2255    pub fn current_home_class_name(&self) -> Option<String> {
2256        match self.get(&self.current_home_class()?) {
2257            Some(JsObj::Class(c)) => Some(c.name.clone()),
2258            _ => None,
2259        }
2260    }
2261
2262    /// Whether `recv` — or anything on its prototype chain — carries the private
2263    /// name `key`. A private FIELD is an own property of the instance; a private
2264    /// METHOD lives on the class prototype, one link up.
2265    pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2266        let mut cur = Some(recv.clone());
2267        while let Some(v) = cur {
2268            let owns = match self.get(&v) {
2269                Some(JsObj::Object(p)) => p.contains_key(key),
2270                Some(JsObj::Class(c)) => c.statics.contains_key(key),
2271                _ => false,
2272            };
2273            if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2274                return true;
2275            }
2276            cur = self.proto_of(&v);
2277        }
2278        false
2279    }
2280
2281    // ── array holes ──────────────────────────────────────────────────────
2282    //
2283    // Every read/write of an array's elision set goes through this block. See
2284    // the `array_holes` field for why the marker lives here rather than in
2285    // `Value`.
2286
2287    /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2288    /// opposed to a stored `undefined`. `false` for anything that is not an
2289    /// array, and for every index of a dense one.
2290    pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2291        match (arr, ()) {
2292            (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2293            _ => false,
2294        }
2295    }
2296
2297    /// Whether `arr` has any elided element at all — one hash probe, and the
2298    /// guard every hole-aware code path takes before doing anything slower.
2299    pub fn has_holes(&self, arr: &Value) -> bool {
2300        matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2301    }
2302
2303    /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2304    /// Sorted because every consumer (own-key enumeration, `util.inspect`
2305    /// run-grouping) needs index order, and the backing set has none.
2306    pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2307        let Value::Obj(i) = arr else {
2308            return Vec::new();
2309        };
2310        let Some(hs) = self.array_holes.get(i) else {
2311            return Vec::new();
2312        };
2313        let mut v: Vec<usize> = hs.iter().copied().collect();
2314        v.sort_unstable();
2315        v
2316    }
2317
2318    /// Record element `i` of `arr` as elided.
2319    pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2320        if let Value::Obj(idx) = arr {
2321            self.array_holes.entry(*idx).or_default().insert(i);
2322        }
2323    }
2324
2325    /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2326    /// the gap a write past the end opens).
2327    pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2328        if range.is_empty() {
2329            return;
2330        }
2331        if let Value::Obj(idx) = arr {
2332            self.array_holes.entry(*idx).or_default().extend(range);
2333        }
2334    }
2335
2336    /// Element `i` now holds a real value: it is no longer a hole. Every write
2337    /// to an array index calls this, which is what keeps a stale hole record
2338    /// from outliving the elision it described.
2339    pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2340        let Value::Obj(idx) = arr else { return };
2341        let Some(hs) = self.array_holes.get_mut(idx) else {
2342            return;
2343        };
2344        hs.remove(&i);
2345        if hs.is_empty() {
2346            self.array_holes.remove(idx);
2347        }
2348    }
2349
2350    /// `arr` is dense from here on (`fill` over the whole array, a fresh
2351    /// dense assignment into an existing handle).
2352    pub fn clear_holes(&mut self, arr: &Value) {
2353        if let Value::Obj(idx) = arr {
2354            self.array_holes.remove(idx);
2355        }
2356    }
2357
2358    /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2359    /// `f`. Used by every method that derives a new array whose holes track the
2360    /// source's (`slice`, `concat`, `map`).
2361    pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2362        if !self.has_holes(src) {
2363            return;
2364        }
2365        let moved: rustc_hash::FxHashSet<usize> =
2366            self.hole_indices(src).into_iter().filter_map(f).collect();
2367        self.install_holes(dst, moved);
2368    }
2369
2370    /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2371    /// existing hole moves to, or `None` if the mutation removed it. This is the
2372    /// one primitive behind every structural array mutation — `shift` is
2373    /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2374    /// `splice` is the general case.
2375    pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2376        if !self.has_holes(arr) {
2377            return;
2378        }
2379        let moved: rustc_hash::FxHashSet<usize> =
2380            self.hole_indices(arr).into_iter().filter_map(f).collect();
2381        self.install_holes(arr, moved);
2382    }
2383
2384    /// Replace `arr`'s elision set outright, dropping the record entirely when
2385    /// the new set is empty so `has_holes` stays a single negative probe for the
2386    /// dense case.
2387    pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2388        let Value::Obj(idx) = arr else { return };
2389        if holes.is_empty() {
2390            self.array_holes.remove(idx);
2391        } else {
2392            self.array_holes.insert(*idx, holes);
2393        }
2394    }
2395
2396    /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2397    /// truncating `splice` leaves behind.
2398    pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2399        self.remap_holes(arr, |i| (i < len).then_some(i));
2400    }
2401
2402    /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2403    /// array, where each maximal run of elided positions collapses to a single
2404    /// `<N empty items>` entry. Returns the entries and whether the last of them
2405    /// is the `... N more items` tail (which the grid layout must not size a
2406    /// column to).
2407    ///
2408    /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2409    /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2410    /// sparse arrays print a single `<200 empty items>`.
2411    fn inspect_sparse(
2412        &self,
2413        v: &Value,
2414        items: &[Value],
2415        indent: usize,
2416        st: &mut InspectCycles,
2417    ) -> (Vec<String>, bool) {
2418        let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2419        let empties = |n: usize| {
2420            let unit = if n == 1 { "item" } else { "items" };
2421            format!("<{n} empty {unit}>")
2422        };
2423        let mut out: Vec<String> = Vec::new();
2424        // The first index not yet accounted for by an entry.
2425        let mut index = 0usize;
2426        for (i, it) in items.iter().enumerate() {
2427            if out.len() >= inspect_max_array_length() {
2428                break;
2429            }
2430            if holes.contains(&i) {
2431                continue;
2432            }
2433            if i > index {
2434                out.push(empties(i - index));
2435                index = i;
2436                if out.len() >= inspect_max_array_length() {
2437                    break;
2438                }
2439            }
2440            out.push(self.inspect_lvl(it, indent + 2, st));
2441            index = i + 1;
2442        }
2443        let remaining = items.len() - index;
2444        if remaining == 0 {
2445            return (out, false);
2446        }
2447        if out.len() < inspect_max_array_length() {
2448            // Trailing holes are still `<N empty items>`, not a truncation.
2449            out.push(empties(remaining));
2450            (out, false)
2451        } else {
2452            let unit = if remaining == 1 { "item" } else { "items" };
2453            out.push(format!("... {remaining} more {unit}"));
2454            (out, true)
2455        }
2456    }
2457    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2458        // Integer-index keys enumerate ascending-first regardless of the order
2459        // they were supplied in (object literal, spread, Object.assign result).
2460        canonicalize_own_keys(&mut props);
2461        // A map carrying the hidden `@@native` tag IS an instance of that native
2462        // class, so it hangs off the class prototype rather than
2463        // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2464        // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2465        // `AbortController` among them — built plain objects instead, so
2466        // `x.constructor.name` read `"Object"` and a chain walk found none of
2467        // the class's methods. Linking HERE means a construction site cannot
2468        // forget it; the tag is already in the map at every one of them.
2469        let tag = props.get("@@native").and_then(|v| self.as_str(v));
2470        let obj = self.alloc(JsObj::Object(props));
2471        if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2472            self.set_proto(&obj, proto);
2473        }
2474        obj
2475    }
2476    pub fn as_str(&self, v: &Value) -> Option<String> {
2477        match v {
2478            Value::Str(s) => Some((**s).clone()),
2479            Value::Obj(_) => match self.get(v) {
2480                Some(JsObj::Str(s)) => Some(s.clone()),
2481                _ => None,
2482            },
2483            _ => None,
2484        }
2485    }
2486
2487    // ── scope / names ────────────────────────────────────────────────────
2488    fn frame(&self) -> &Frame {
2489        self.frames.last().unwrap()
2490    }
2491    fn cur_env(&self) -> Env {
2492        self.frame().env.clone()
2493    }
2494
2495    // ── DAP debug introspection (used only under `--dap`) ────────────────────
2496    /// Number of active call frames (the debugger's step-depth reference).
2497    pub fn frame_depth(&self) -> usize {
2498        self.frames.len()
2499    }
2500    /// Record the source line the innermost frame is executing (DAP line hook).
2501    pub fn set_cur_line(&mut self, line: u32) {
2502        if let Some(f) = self.frames.last_mut() {
2503            f.line = line;
2504        }
2505    }
2506    /// The `.stack` tail for an error created right now: one `    at <name>`
2507    /// line per live frame, innermost first, ending at the module frame.
2508    ///
2509    /// These are the REAL user frames — node-js has no `file:line:column` (the
2510    /// per-frame line is only tracked under `--dap`) and no Node-internal
2511    /// module-loader frames, so `.stack` names the call chain but can never be
2512    /// byte-identical to V8's. The names are what makes a thrown error
2513    /// diagnosable; the missing positions are documented in BUGS.md.
2514    /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2515    ///
2516    /// The default is 10, it is settable, and setting it to 0 is the documented
2517    /// way to make error construction cheap. It did not exist, so the read was
2518    /// `undefined` and every stack carried every frame regardless.
2519    pub fn stack_trace_limit(&self) -> usize {
2520        match self.builtin_static("Error", "stackTraceLimit") {
2521            Some(v) => {
2522                let n = self.to_number(&v);
2523                if n.is_finite() && n > 0.0 {
2524                    n as usize
2525                } else if n.is_nan() || n <= 0.0 {
2526                    0
2527                } else {
2528                    usize::MAX
2529                }
2530            }
2531            None => 10,
2532        }
2533    }
2534
2535    pub fn stack_frames(&self) -> String {
2536        let limit = self.stack_trace_limit();
2537        if limit == 0 {
2538            return String::new();
2539        }
2540        let mut out = String::new();
2541        for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2542            let name = match (&f.owner, i) {
2543                (Some(n), _) => n.clone(),
2544                (None, 0) => "Object.<anonymous>".to_string(),
2545                (None, _) => "<anonymous>".to_string(),
2546            };
2547            out.push_str("\n    at ");
2548            out.push_str(&name);
2549        }
2550        if out.is_empty() && limit > 0 {
2551            out.push_str("\n    at <anonymous>");
2552        }
2553        out
2554    }
2555
2556    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2557    /// `stackTrace`. `owner` carries the function name where known.
2558    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2559        self.frames
2560            .iter()
2561            .rev()
2562            .map(|f| {
2563                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2564                (name, f.line)
2565            })
2566            .collect()
2567    }
2568    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2569    pub fn dbg_locals(&self) -> Vec<(String, String)> {
2570        let env = self.cur_env();
2571        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2572        names
2573            .into_iter()
2574            .map(|n| {
2575                let v = self.read_name(&n).unwrap_or(Value::Undef);
2576                (n, self.inspect(&v))
2577            })
2578            .collect()
2579    }
2580
2581    /// Scope-chain read: local + enclosing chain, then globals.
2582    /// Whether `name` is a module-top-level binding that has not reached its
2583    /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2584    /// block-scoped one by inspecting the value it holds.
2585    pub fn is_tdz_global(&self, name: &str) -> bool {
2586        self.tdz_globals.contains(name)
2587    }
2588
2589    pub fn read_name(&self, name: &str) -> Option<Value> {
2590        let mut env = Some(self.cur_env());
2591        while let Some(e) = env {
2592            if let Some(v) = e.borrow().vars.get(name) {
2593                return Some(v.clone());
2594            }
2595            env = e.borrow().parent.clone();
2596        }
2597        self.globals.get(name).cloned()
2598    }
2599    pub fn read_global(&self, name: &str) -> Option<Value> {
2600        self.globals.get(name).cloned()
2601    }
2602
2603    /// Whether `name` is bound anywhere on the scope chain or in the globals —
2604    /// `read_name(..).is_some()` without cloning the value it finds. The
2605    /// strict-mode assignment path asks this and nothing else.
2606    pub fn has_name(&self, name: &str) -> bool {
2607        let mut env = Some(self.cur_env());
2608        while let Some(e) = env {
2609            if e.borrow().vars.contains_key(name) {
2610                return true;
2611            }
2612            env = e.borrow().parent.clone();
2613        }
2614        self.globals.contains_key(name)
2615    }
2616
2617    /// Assign to an existing binding up the scope chain, else create a global
2618    /// (JS assignment to an undeclared name targets the global object).
2619    /// Assign to an existing binding, or create a global. Returns `false` when
2620    /// the nearest binding is an immutable (`const`) one, which the caller turns
2621    /// into `TypeError: Assignment to constant variable.` — assigning to a
2622    /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2623    /// a mutated constant.
2624    #[must_use]
2625    pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2626        let mut env = Some(self.cur_env());
2627        while let Some(e) = env {
2628            // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2629            // binding hashed the name twice and allocated a fresh `String` key
2630            // for a key that was already there — once per assignment, so once
2631            // per loop iteration in any counting loop.
2632            //
2633            // The const check runs only at the env that OWNS the name, and the
2634            // `is_empty` guard settles the common (no consts here) case without
2635            // hashing the name again.
2636            let mut b = e.borrow_mut();
2637            if b.vars.contains_key(name) {
2638                if !b.consts.is_empty() && b.consts.contains(name) {
2639                    return false;
2640                }
2641                if let Some(slot) = b.vars.get_mut(name) {
2642                    *slot = val;
2643                }
2644                return true;
2645            }
2646            drop(b);
2647            env = e.borrow().parent.clone();
2648        }
2649        if self.global_consts.contains(name) {
2650            return false;
2651        }
2652        match self.globals.get_mut(name) {
2653            Some(slot) => *slot = val,
2654            None => {
2655                self.globals.insert(name.to_string(), val);
2656            }
2657        }
2658        true
2659    }
2660
2661    /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2662    /// plus recording the name as immutable in whichever scope received it.
2663    pub fn declare_const_name(&mut self, name: &str, val: Value) {
2664        let f = self.frame();
2665        let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2666        self.declare_name(name, val);
2667        if to_globals {
2668            self.global_consts.insert(name.to_string());
2669        } else {
2670            self.cur_env().borrow_mut().consts.insert(name.to_string());
2671        }
2672    }
2673
2674    /// The value a lexical binding holds between entering its scope and reaching
2675    /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2676    /// process, so the check is a heap-index comparison and the marker cannot be
2677    /// produced by any JavaScript expression. It never escapes: every path that
2678    /// could read it throws first.
2679    pub fn tdz_marker(&mut self) -> Value {
2680        if let Some(v) = &self.tdz {
2681            return v.clone();
2682        }
2683        let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2684        self.tdz = Some(v.clone());
2685        v
2686    }
2687
2688    /// Whether `v` is the uninitialized-binding marker.
2689    pub fn is_tdz(&self, v: &Value) -> bool {
2690        matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2691    }
2692
2693    /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2694    /// already binds it. Emitted at the top of every scope for each `let`,
2695    /// `const` and `class` declared directly in it, so a read before the
2696    /// declaration throws instead of finding an OUTER binding of the same name —
2697    /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2698    pub fn hoist_tdz(&mut self, name: &str) {
2699        let marker = self.tdz_marker();
2700        let f = self.frame();
2701        // At module top level a lexical binding lives in `globals`, which is ALSO
2702        // what backs `globalThis.<name>` — so parking the marker there exposes it
2703        // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2704        // back as the marker. Top-level dead zones are tracked in a separate set
2705        // that only the name-read path consults.
2706        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2707            if !self.globals.contains_key(name) {
2708                self.tdz_globals.insert(name.to_string());
2709            }
2710            return;
2711        }
2712        let env = self.cur_env();
2713        let mut e = env.borrow_mut();
2714        if !e.vars.contains_key(name) {
2715            e.vars.insert(name.to_string(), marker);
2716        }
2717    }
2718
2719    /// Declare a new binding in the current scope (`let`/`const`). At the top of
2720    /// the module frame there is no local env, so those names become globals; once
2721    /// a block scope is open the binding belongs to that block.
2722    pub fn declare_name(&mut self, name: &str, val: Value) {
2723        let f = self.frame();
2724        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2725            self.tdz_globals.remove(name);
2726            self.globals.insert(name.to_string(), val);
2727        } else {
2728            self.cur_env()
2729                .borrow_mut()
2730                .vars
2731                .insert(name.to_string(), val);
2732        }
2733    }
2734
2735    /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2736    /// skips every open block scope and lands in the activation's base env.
2737    /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2738    /// name is not already bound in this activation.
2739    ///
2740    /// `var` bindings come into existence when the scope is entered, not where
2741    /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2742    /// rather than throwing. "If absent" is what keeps a parameter intact: in
2743    /// `function f(a) { var a; }` the `var` names a binding that already exists
2744    /// and must not be reset, which is also why a bare `var x;` emits nothing at
2745    /// its own position.
2746    pub fn hoist_var_name(&mut self, name: &str) {
2747        // The ENTRY script's top level is a CommonJS module body, not global
2748        // scope: node wraps every file in a function, so a top-level `var` is a
2749        // local of that wrapper. Binding it into the globals map made
2750        // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2751        // node says `undefined` — a REQUIRED module already ran inside a real
2752        // frame and behaved correctly, so only the entry file differed.
2753        if self.frame().is_module && !self.module_scope {
2754            self.globals.entry(name.to_string()).or_insert(Value::Undef);
2755            return;
2756        }
2757        let base = self.frame().base_env.clone();
2758        let mut env = base.borrow_mut();
2759        if !env.vars.contains_key(name) {
2760            env.vars.insert(name.to_string(), Value::Undef);
2761        }
2762    }
2763
2764    pub fn declare_var_name(&mut self, name: &str, val: Value) {
2765        if self.frame().is_module && !self.module_scope {
2766            self.globals.insert(name.to_string(), val);
2767            return;
2768        }
2769        let base = self.frame().base_env.clone();
2770        base.borrow_mut().vars.insert(name.to_string(), val);
2771    }
2772
2773    /// Enter a fresh block scope.
2774    pub fn push_scope(&mut self) {
2775        let env = self.cur_env();
2776        self.frames.last_mut().unwrap().env = child_env(env);
2777    }
2778
2779    /// Open a scope that is also the activation's VARIABLE environment, and
2780    /// return the previous one so the caller can restore it.
2781    ///
2782    /// A block scope is not enough for a strict direct `eval`: `var` and a
2783    /// hoisted function declaration bind to `base_env`, so they walked straight
2784    /// past a plain `push_scope` and still landed in the caller's function
2785    /// scope. Only `let`/`const` were contained.
2786    pub fn push_var_scope(&mut self) -> Env {
2787        let env = child_env(self.cur_env());
2788        let f = self.frames.last_mut().unwrap();
2789        let prev = std::mem::replace(&mut f.base_env, env.clone());
2790        f.env = env;
2791        prev
2792    }
2793
2794    /// Restore the variable environment a `push_var_scope` replaced.
2795    pub fn pop_var_scope(&mut self, prev: Env) {
2796        let f = self.frames.last_mut().unwrap();
2797        f.env = prev.clone();
2798        f.base_env = prev;
2799    }
2800
2801    /// Leave the innermost block scope (never pops past the activation's base).
2802    pub fn pop_scope(&mut self) {
2803        let cur = self.cur_env();
2804        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2805            return;
2806        }
2807        let parent = cur.borrow().parent.clone();
2808        if let Some(p) = parent {
2809            self.frames.last_mut().unwrap().env = p;
2810        }
2811    }
2812
2813    /// Replace the innermost block scope with a fresh copy of its bindings — the
2814    /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2815    /// in one iteration keeps that iteration's value.
2816    pub fn copy_scope(&mut self) {
2817        let cur = self.cur_env();
2818        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2819            return;
2820        }
2821        let parent = cur.borrow().parent.clone();
2822        let fresh = new_env(parent);
2823        fresh.borrow_mut().vars = cur.borrow().vars.clone();
2824        self.frames.last_mut().unwrap().env = fresh;
2825    }
2826
2827    /// The current block-scope env, for save/restore across a nested chunk.
2828    pub fn scope_snapshot(&self) -> Env {
2829        self.cur_env()
2830    }
2831    pub fn restore_scope(&mut self, env: Env) {
2832        self.frames.last_mut().unwrap().env = env;
2833    }
2834    pub fn set_global(&mut self, name: &str, val: Value) {
2835        self.globals.insert(name.to_string(), val);
2836    }
2837
2838    // ── output capture ───────────────────────────────────────────────────
2839    //
2840    // Every write a *program* makes — `console.log`, `process.stdout.write`,
2841    // `print` — funnels through `write_out`, so turning capture on redirects all
2842    // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2843    // crash traceback from `main`) deliberately do not: they belong to the
2844    // process, not to the program.
2845
2846    /// Start capturing program output in-process. Any text already captured is
2847    /// discarded, so each run starts clean.
2848    pub fn begin_capture(&mut self) {
2849        self.capture = Some(Vec::new());
2850    }
2851
2852    /// Stop capturing and take everything written since [`begin_capture`],
2853    /// returning the empty string when capture was not on. The captured bytes
2854    /// are rendered lossily: this API hands back a `String`, so a program that
2855    /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2856    /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2857    ///
2858    /// [`begin_capture`]: JsHost::begin_capture
2859    /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2860    pub fn end_capture(&mut self) -> String {
2861        String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2862    }
2863
2864    /// Stop capturing and take the raw bytes, without the lossy transcription
2865    /// [`end_capture`] applies.
2866    ///
2867    /// [`end_capture`]: JsHost::end_capture
2868    pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2869        self.capture.take().unwrap_or_default()
2870    }
2871
2872    /// Whether output is being captured — the one thing a caller needs to know
2873    /// before asking the real stream a question (`isTTY`, cursor position).
2874    pub fn capturing(&self) -> bool {
2875        self.capture.is_some()
2876    }
2877
2878    /// Write program output: into the capture buffer when capturing, else to the
2879    /// process stream `stderr` selects. `s` is written verbatim — callers add
2880    /// their own line ending, as `console.log` does and `process.stdout.write`
2881    /// does not.
2882    pub fn write_out(&mut self, s: &str, stderr: bool) {
2883        self.write_out_bytes(s.as_bytes(), stderr);
2884    }
2885
2886    /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2887    /// a byte string and Node writes it through untouched, so a `Buffer` holding
2888    /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2889    /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2890    /// became seven — so the byte path exists separately from [`write_out`].
2891    ///
2892    /// [`write_out`]: JsHost::write_out
2893    pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2894        if let Some(buf) = &mut self.capture {
2895            buf.extend_from_slice(bytes);
2896            return;
2897        }
2898        use std::io::Write as _;
2899        if stderr {
2900            let mut e = std::io::stderr();
2901            let _ = e.write_all(bytes);
2902            let _ = e.flush();
2903        } else {
2904            let mut o = std::io::stdout();
2905            let _ = o.write_all(bytes);
2906            let _ = o.flush();
2907        }
2908    }
2909    pub fn del_name(&mut self, name: &str) {
2910        if self
2911            .cur_env()
2912            .borrow_mut()
2913            .vars
2914            .shift_remove(name)
2915            .is_some()
2916        {
2917            return;
2918        }
2919        self.globals.shift_remove(name);
2920    }
2921
2922    pub fn current_this(&self) -> Option<Value> {
2923        self.frame().this_obj.clone()
2924    }
2925
2926    /// The running activation's [`ThisState`].
2927    pub fn this_state(&self) -> ThisState {
2928        self.frame().this_state
2929    }
2930
2931    /// Mark the next user-function activation as a derived constructor.
2932    pub fn mark_next_call_derived_ctor(&mut self) {
2933        self.derived_ctor_next = true;
2934    }
2935
2936    /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2937    /// nearest derived-constructor activation becomes `Bound`. That is the top
2938    /// frame, or — for `super()` inside an arrow — the constructor below the
2939    /// arrow's own frame. `false` when it was already bound: the second call.
2940    pub fn bind_super_this(&mut self) -> bool {
2941        let Some(f) = self
2942            .frames
2943            .iter_mut()
2944            .rev()
2945            .find(|f| f.this_state != ThisState::Plain)
2946        else {
2947            return true;
2948        };
2949        if f.this_state == ThisState::Bound {
2950            return false;
2951        }
2952        f.this_state = ThisState::Bound;
2953        true
2954    }
2955
2956    /// The object a `super()` call substituted for the instance, if any.
2957    ///
2958    /// `construct_class` allocates the instance up front, so when a base
2959    /// constructor RETURNS an object the substitution happens deep inside the
2960    /// VM, after that allocation. This carries it back out. Each
2961    /// `construct_class` saves and restores the previous value around its own
2962    /// run, so a `new` inside a constructor body cannot steal it.
2963    pub fn take_super_replacement(&mut self) -> Option<Value> {
2964        self.super_replacement.take()
2965    }
2966
2967    pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2968        std::mem::replace(&mut self.super_replacement, v)
2969    }
2970
2971    /// Rebind the running activation's `this`.
2972    ///
2973    /// Only `super()` does this: when the parent constructor RETURNS an object,
2974    /// 15.7.15 makes that object the derived instance, so the rest of the
2975    /// derived constructor has to write to it rather than to the one allocated
2976    /// before the call.
2977    pub fn set_current_this(&mut self, v: Value) {
2978        if let Some(f) = self.frames.last_mut() {
2979            f.this_obj = Some(v.clone());
2980        }
2981        self.super_replacement = Some(v);
2982    }
2983    /// The callbacks to run for `event`, consuming any `once` registration in
2984    /// the same step — so a listener that re-emits the event cannot re-enter a
2985    /// one-shot handler.
2986    pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2987        let Some(list) = self.process_listeners.get_mut(event) else {
2988            return Vec::new();
2989        };
2990        let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2991        list.retain(|l| !l.once);
2992        fired
2993    }
2994
2995    /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2996    ///
2997    /// Node answers differently per entry point and both answers are objects:
2998    /// `node f.js` runs a CommonJS module, so top-level `this` is
2999    /// `module.exports`; `node -e` and `node -` run a Script, so it is
3000    /// `globalThis`. Verified on node v26.7.0 —
3001    /// `console.log(this === globalThis, this === module.exports)` is
3002    /// `false true` from a file and `true false` from `-e` and from stdin. It
3003    /// was `undefined` at every entry point here, so `this.x = 1` at module
3004    /// scope threw instead of populating the exports object.
3005    ///
3006    /// Only the base frame is touched: a plain function call still gets its own
3007    /// (`undefined`) binding rather than inheriting this one.
3008    pub fn set_top_this(&mut self, v: Value) {
3009        if let Some(f) = self.frames.first_mut() {
3010            f.this_obj = Some(v);
3011        }
3012    }
3013    pub fn current_env_capture(&self) -> Env {
3014        self.frame().env.clone()
3015    }
3016    pub fn current_new_target(&self) -> Option<Value> {
3017        self.frame().new_target.clone()
3018    }
3019    fn current_home_class(&self) -> Option<Value> {
3020        self.frame().home_class.clone()
3021    }
3022
3023    /// The `(parent_ctor, this_class_fields)` for a running constructor's
3024    /// `super(...)`, derived from the frame's home class.
3025    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3026        match self.current_home_class() {
3027            Some(cv) => match self.get(&cv) {
3028                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3029                _ => (None, Vec::new()),
3030            },
3031            None => (None, Vec::new()),
3032        }
3033    }
3034
3035    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3036    /// by the caller, outside any host borrow) or a directly-usable value.
3037    pub fn super_resolve(&self, name: &str) -> SuperRef {
3038        // A shorthand method in an OBJECT LITERAL resolves `super` through its
3039        // home object's prototype; only a class method has a home CLASS. With
3040        // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3041        // parent to look in and reported the method missing.
3042        if let Some(home) = self.frame().home_object.clone() {
3043            let target = self.proto_of(&home).unwrap_or(Value::Undef);
3044            if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3045                return SuperRef::Getter(getter);
3046            }
3047            return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3048        }
3049        let parent = match self
3050            .current_home_class()
3051            .and_then(|cv| match self.get(&cv) {
3052                Some(JsObj::Class(c)) => c.parent.clone(),
3053                _ => None,
3054            }) {
3055            Some(p) => p,
3056            None => return SuperRef::Data(Value::Undef),
3057        };
3058        // A STATIC method's home object is the constructor, so `super.x` reads
3059        // off the parent CONSTRUCTOR; an instance method's is the prototype
3060        // object, so it reads off the parent's prototype. Always taking the
3061        // prototype meant `static s() { return super.s(); }` found nothing and
3062        // then tried to call it.
3063        let target = if self.frame().home_static {
3064            parent.clone()
3065        } else {
3066            match self.get(&parent) {
3067                Some(JsObj::Class(pc)) => pc.proto.clone(),
3068                _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3069            }
3070        };
3071        if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3072            return SuperRef::Getter(getter);
3073        }
3074        if let Some(v) = lookup_chain(self, &target, name) {
3075            return SuperRef::Data(v);
3076        }
3077        // A static method lives in the fn-prop side table, not the property map.
3078        SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3079    }
3080
3081    // ── signals / errors ─────────────────────────────────────────────────
3082    pub fn take_error(&mut self) -> Option<String> {
3083        self.error.take()
3084    }
3085    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3086        let s = if msg.is_empty() {
3087            class.to_string()
3088        } else {
3089            format!("{class}: {msg}")
3090        };
3091        self.error = Some(s.clone());
3092        s
3093    }
3094}
3095
3096// ── error constructors ───────────────────────────────────────────────────────
3097
3098pub fn type_error(msg: &str) -> String {
3099    format!("TypeError: {msg}")
3100}
3101pub fn ref_error(name: &str) -> String {
3102    format!("ReferenceError: {name} is not defined")
3103}
3104
3105/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3106/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3107/// happened, and the difference is how a reader tells a misspelled name from a
3108/// `let` used above its declaration.
3109pub fn tdz_error(name: &str) -> String {
3110    format!("ReferenceError: Cannot access '{name}' before initialization")
3111}
3112pub fn range_error(msg: &str) -> String {
3113    format!("RangeError: {msg}")
3114}
3115
3116/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3117/// largest string the engine will materialize.
3118///
3119/// Measured on node v26.7.0 (darwin arm64):
3120/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3121/// `'a'.repeat(536870888)` succeeds with that length, and
3122/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3123pub const MAX_STRING_LENGTH: usize = 536_870_888;
3124
3125/// The error V8 raises for a string operation whose RESULT would exceed
3126/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3127/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3128/// sit building a 1 TiB `String` until it was killed.
3129pub fn invalid_string_length() -> String {
3130    range_error("Invalid string length")
3131}
3132
3133/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3134/// and 10.4.2.4 `ArraySetLength` step 3.
3135///
3136/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3137/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3138/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3139/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3140/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3141/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3142/// materializing four billion elements for `a.length = 2**32`.
3143pub fn to_array_length(v: &Value) -> Result<usize, String> {
3144    // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3145    // `ToNumber(value)`, compared against each other. Both are observable — a
3146    // counting `valueOf` sees two calls in node and saw one here — and the
3147    // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3148    let u32_pass = to_number_value(v)?;
3149    let n = to_number_value(v)?;
3150    let _ = u32_pass;
3151    // `ToUint32`: truncate toward zero, then modulo 2^32.
3152    let u = if n.is_finite() {
3153        (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3154    } else {
3155        0
3156    };
3157    // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3158    if (u as f64) != n {
3159        return Err(range_error("Invalid array length"));
3160    }
3161    Ok(u as usize)
3162}
3163
3164/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3165///
3166/// `builtins::synth_error` parses that head back apart, so the bracketed code
3167/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3168/// handling reads. Writing the head by hand at each throw site is what left a
3169/// dozen of them with `err.code === undefined` while the message matched.
3170///
3171/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3172/// is left bracketed while the stack is captured and therefore shows up in both
3173/// `String(err)` and `err.stack` — measured on v26.7.0:
3174///
3175/// ```text
3176/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3177/// ```
3178pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3179    format!("{class} [{code}]: {msg}")
3180}
3181
3182/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3183pub const CODE_MARK: &str = "\u{1}code:";
3184
3185/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3186/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3187/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3188/// `synth_error` could otherwise recognise.
3189pub const DOM_MARK: &str = "\u{1}dom:";
3190
3191/// A `DOMException` error string: `name` is the WHATWG error name.
3192pub fn dom_error(name: &str, msg: &str) -> String {
3193    format!("{DOM_MARK}{name}\u{1}{msg}")
3194}
3195
3196/// A Node coded error raised from the *native* layer: `.code` is set, but the
3197/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3198///
3199/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3200/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3201/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3202/// brackets its code into the very same two reads. Encoding both through one
3203/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3204///
3205/// The code rides in a marker at the head of the message rather than in the
3206/// error class, because the class text is exactly what must NOT carry it. The
3207/// marker is an internal wire format between a throw site and `synth_error`; it
3208/// never survives into a `.message`.
3209pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3210    format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3211}
3212
3213/// Marks the start of the extra string own properties a
3214/// [`plain_coded_error_with`] error carries after its message.
3215pub const FIELDS_MARK: char = '\u{2}';
3216
3217/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3218/// `code` in the order given — `new URL('x', 'nope')` throws with
3219/// `Object.keys(e)` reading `["code","input","base"]`.
3220///
3221/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3222/// arbitrary user text) may carry any character, the separators included.
3223pub fn plain_coded_error_with(
3224    class: &str,
3225    code: &str,
3226    msg: &str,
3227    fields: &[(&str, &str)],
3228) -> String {
3229    let mut s = plain_coded_error(class, code, msg);
3230    s.push(FIELDS_MARK);
3231    for (k, v) in fields {
3232        s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3233    }
3234    s
3235}
3236
3237/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3238/// internal code and field markers of [`plain_coded_error`] /
3239/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3240/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3241pub fn plain_error_text(e: &str) -> String {
3242    let Some(i) = e.find(CODE_MARK) else {
3243        return e.to_string();
3244    };
3245    let (head, rest) = e.split_at(i);
3246    match rest[CODE_MARK.len()..].split_once('\u{1}') {
3247        Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3248        None => e.to_string(),
3249    }
3250}
3251
3252/// Split a [`plain_coded_error_with`] message back into the message and its
3253/// fields. A message with no field mark comes back whole with no fields.
3254pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3255    let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3256        return (msg, Vec::new());
3257    };
3258    let mut fields = Vec::new();
3259    while let Some((k, tail)) = rest.split_once('\u{3}') {
3260        let Some((len, tail)) = tail.split_once('\u{3}') else {
3261            break;
3262        };
3263        let Ok(len) = len.parse::<usize>() else { break };
3264        let Some(v) = tail.get(..len) else { break };
3265        fields.push((k, v));
3266        rest = &tail[len..];
3267    }
3268    (head, fields)
3269}
3270
3271/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3272/// <expected>. Received …` — Node's single most common argument rejection.
3273pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3274    coded_error(
3275        "TypeError",
3276        "ERR_INVALID_ARG_TYPE",
3277        &format!(
3278            "The \"{name}\" {kind} must be of type {expected}. Received {}",
3279            crate::stdlib::received_desc(v)
3280        ),
3281    )
3282}
3283
3284// ── the fusevm run plumbing ──────────────────────────────────────────────────
3285
3286thread_local! {
3287    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3288}
3289
3290/// Enable/disable DAP debug execution (`node --dap`).
3291pub fn set_debug_mode(on: bool) {
3292    DEBUG_MODE.with(|d| d.set(on));
3293}
3294
3295// ── join cycle detection ─────────────────────────────────────────────────────
3296
3297thread_local! {
3298    /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3299    static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3300}
3301
3302/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3303/// it already is.
3304///
3305/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3306/// it) is the one place the language walks an object graph with no depth bound,
3307/// so every engine cuts re-entrance here: a receiver already on the stack
3308/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3309/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3310/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3311/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3312/// uncatchable, where node returns a string.
3313///
3314/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3315/// twice, because the first render pops before the second pushes.
3316///
3317/// A `true` return MUST be paired with [`join_stack_pop`].
3318pub fn join_stack_push(v: &Value) -> bool {
3319    match v {
3320        Value::Obj(i) => JOIN_STACK.with(|s| {
3321            let mut s = s.borrow_mut();
3322            if s.contains(i) {
3323                false
3324            } else {
3325                s.push(*i);
3326                true
3327            }
3328        }),
3329        _ => true,
3330    }
3331}
3332
3333/// Pop the innermost [`join_stack_push`].
3334pub fn join_stack_pop() {
3335    JOIN_STACK.with(|s| {
3336        s.borrow_mut().pop();
3337    });
3338}
3339
3340// ── native stack guard ───────────────────────────────────────────────────────
3341
3342thread_local! {
3343    /// Lowest stack address a nested run may start from, or 0 before the
3344    /// running thread's bounds have been measured. Cached because the pthread
3345    /// query is a syscall-free but non-trivial read and this is on every call.
3346    static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3347}
3348
3349/// Stack left unusable below the floor, as a fraction of the whole stack: the
3350/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3351/// whatever `catch` receives it, all of which needs room *below* the deepest
3352/// call that was allowed.
3353const STACK_RESERVE_DIVISOR: usize = 8;
3354/// Floor of that reserve, for a thread whose stack is small enough that an
3355/// eighth of it would not cover the unwind.
3356const STACK_RESERVE_MIN: usize = 512 * 1024;
3357/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3358/// large relative to a default 8 MiB stack — over-reserving costs recursion
3359/// depth, under-reserving costs the process.
3360const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3361
3362/// The address of a local in the caller's frame — how far down the stack
3363/// execution currently is. `black_box` keeps the probe from being optimized into
3364/// a different frame.
3365fn stack_pointer() -> usize {
3366    let probe = 0u8;
3367    std::hint::black_box(&probe) as *const u8 as usize
3368}
3369
3370/// The running thread's `(lowest address, size)` stack bounds.
3371///
3372/// Asked of pthread rather than assumed, because the three threads that run JS
3373/// have three different stacks: the `node` binary's own (`main.rs` reserves
3374/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3375/// harness thread's. A fixed byte budget would be wrong on two of the three.
3376fn stack_bounds() -> Option<(usize, usize)> {
3377    #[cfg(target_vendor = "apple")]
3378    {
3379        // SAFETY: both calls are pure reads of the calling thread's own
3380        // pthread record; neither allocates nor can fail.
3381        unsafe {
3382            let me = libc::pthread_self();
3383            let top = libc::pthread_get_stackaddr_np(me) as usize;
3384            let size = libc::pthread_get_stacksize_np(me);
3385            if size == 0 || top < size {
3386                return None;
3387            }
3388            Some((top - size, size))
3389        }
3390    }
3391    #[cfg(target_os = "linux")]
3392    {
3393        // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3394        // read, only read on the success path, and destroyed on every path.
3395        unsafe {
3396            let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3397            if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3398                return None;
3399            }
3400            let mut low: *mut libc::c_void = std::ptr::null_mut();
3401            let mut size: libc::size_t = 0;
3402            let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3403            libc::pthread_attr_destroy(&mut attr);
3404            if ok && size != 0 {
3405                return Some((low as usize, size));
3406            }
3407            None
3408        }
3409    }
3410    #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3411    {
3412        None
3413    }
3414}
3415
3416/// The stack address below which a further nested VM run must throw instead of
3417/// recursing.
3418///
3419/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3420/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3421/// and runs the body, whose own calls land back here. Unbounded JS recursion
3422/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3423/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3424/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3425/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3426///
3427/// The floor is derived from the thread's real bounds rather than a frame count
3428/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3429/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3430/// stack and no more), a release build far less, and a native builtin recursing
3431/// through a user callback spends a different amount again.
3432fn stack_floor() -> usize {
3433    let cached = STACK_FLOOR.with(|c| c.get());
3434    if cached != 0 {
3435        return cached;
3436    }
3437    let floor = match stack_bounds() {
3438        Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3439        None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3440    };
3441    STACK_FLOOR.with(|c| c.set(floor));
3442    floor
3443}
3444
3445/// Stack given to each generator/async coroutine.
3446///
3447/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3448/// left a `function*` body barely ten frames of recursion before it walked off
3449/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3450/// a larger one is address space, not resident memory — but it IS per live
3451/// generator, so this stays far below the entry thread's
3452/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3453/// has thousands of these.
3454const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3455
3456/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3457fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3458    stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3459}
3460
3461/// corosensei's own `DefaultStack::default()` size, used only when the
3462/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3463/// on a stack whose bounds are not ours to read.
3464const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3465
3466/// Give a coroutine whose stack bounds are unknown a floor measured from where
3467/// its body starts. Called once, at body entry, on the coroutine's own stack.
3468fn ensure_coroutine_floor() {
3469    if STACK_FLOOR.with(|c| c.get()) != 0 {
3470        return;
3471    }
3472    let budget = CORO_FALLBACK_STACK_SIZE
3473        - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3474    STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3475}
3476
3477/// Install `floor` as the current stack floor, returning the previous one.
3478///
3479/// Used around a coroutine resume, which switches to a stack the thread's
3480/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3481/// the next [`stack_floor`] measure again, which is the right answer for the
3482/// entry thread and a conservative one for a fallback coroutine stack.
3483fn swap_stack_floor(floor: usize) -> usize {
3484    STACK_FLOOR.with(|c| c.replace(floor))
3485}
3486
3487/// Whether the native stack is too close to its floor for one more nested run.
3488pub fn stack_exhausted() -> bool {
3489    stack_pointer() <= stack_floor()
3490}
3491
3492/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3493/// `RangeError` constructor node uses — not a `panic!`.
3494pub fn stack_overflow_error() -> String {
3495    range_error("Maximum call stack size exceeded")
3496}
3497
3498/// Pool key for the body of user function `def_id`.
3499pub fn func_key(def_id: usize) -> u64 {
3500    1 << 40 | def_id as u64
3501}
3502
3503/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3504/// handler, 2 = the finalizer.
3505pub fn try_key(try_id: usize, part: u64) -> u64 {
3506    2 << 40 | (try_id as u64) << 2 | part
3507}
3508
3509thread_local! {
3510    /// VMs that have finished a run, kept for the next one — grouped by the
3511    /// chunk they still hold.
3512    ///
3513    /// Every JS call, every `try` block and every generator step runs its chunk
3514    /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3515    /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3516    /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3517    /// 400k calls, so it built 400k VMs to run 23 ops each.
3518    ///
3519    /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3520    /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3521    /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3522    /// call takes back the VM that already holds that body and copies nothing:
3523    /// `VM::reset` is handed the chunk the VM was already carrying.
3524    ///
3525    /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3526    /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3527    /// a nested (or recursive) call takes the next one, so a key grows to the
3528    /// deepest simultaneous entry into that function and no further.
3529    static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3530        RefCell::new(rustc_hash::FxHashMap::default());
3531}
3532
3533/// An idle VM filed under `key`, if any.
3534fn take_pooled(key: u64) -> Option<VM> {
3535    VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3536}
3537
3538/// File a finished VM under `key` for the next run to take.
3539fn put_pooled(key: u64, vm: VM) {
3540    VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3541}
3542
3543/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3544/// and fitted with the builtins and hooks a fresh VM needs.
3545fn acquire_vm(chunk: Chunk) -> VM {
3546    if let Some(mut vm) = take_pooled(0) {
3547        vm.reset(chunk);
3548        return vm;
3549    }
3550    let mut vm = VM::new(chunk);
3551    crate::builtins::install(&mut vm);
3552    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3553        crate::builtins::numeric_hook(op, a, b)
3554    }));
3555    // Under `--dap` the tracing JIT would compile hot loops and skip the
3556    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3557    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3558    // extension seam mirrors pythonrs should the marker emission ever switch.
3559    // The mode is fixed before the first chunk runs, so a pooled VM can never
3560    // come back wearing the wrong one.
3561    if DEBUG_MODE.with(|d| d.get()) {
3562        vm.set_extension_handler(Box::new(|vm, id, _| {
3563            crate::dap::on_ext(vm, id);
3564        }));
3565    } else {
3566        vm.enable_tracing_jit();
3567    }
3568    vm
3569}
3570
3571/// Register every node-js builtin + the numeric hook on a VM, then run it.
3572///
3573/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3574/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3575/// that runs repeatedly (a function body, a `try` block) goes through
3576/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3577pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3578    // Checked before the `VM` is built: `VM::new` + `install` are themselves
3579    // several KiB of frame, so a check after them could already have overflowed.
3580    if stack_exhausted() {
3581        return Err(stack_overflow_error());
3582    }
3583    finish_run(0, acquire_vm(chunk))
3584}
3585
3586/// Run the chunk filed under `key`, building it with `make` only if no VM is
3587/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3588/// call copies no bytecode at all.
3589pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3590    if stack_exhausted() {
3591        return Err(stack_overflow_error());
3592    }
3593    let vm = match take_pooled(key) {
3594        Some(mut vm) => {
3595            // Hand the VM back the chunk it is already carrying: `reset` takes
3596            // an owned `Chunk`, and this is the one place where the owned chunk
3597            // costs nothing.
3598            let held = std::mem::take(&mut vm.chunk);
3599            vm.reset(held);
3600            vm
3601        }
3602        None => acquire_vm(make()),
3603    };
3604    finish_run(key, vm)
3605}
3606
3607/// Run a prepared VM to completion and file it back under `key`.
3608fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3609    let outcome = vm.run();
3610    let result = match outcome {
3611        _ if with_host(|h| h.error.is_some()) => {
3612            Err(with_host(|h| h.take_error()).expect("just checked"))
3613        }
3614        VMResult::Ok(v) => Ok(v),
3615        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3616        VMResult::Error(e) => Err(e),
3617    };
3618    put_pooled(key, vm);
3619    result
3620}
3621
3622/// Run `chunk` in the GLOBAL scope instead of the caller's.
3623///
3624/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3625/// and can shadow — the *calling function's* locals. That is right for a direct
3626/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3627/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3628/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3629/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3630/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3631/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3632/// is `"undefined"` there and was `"number"` here.
3633///
3634/// A `var` the chunk itself declares lands in the top-level scope and persists,
3635/// so successive `vm.runInThisContext` calls share it.
3636pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3637    // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3638    // to the global object, not to the entry module's wrapper scope. The flag
3639    // that keeps the entry script's own `var`s out of the globals map has to be
3640    // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3641    // `globalThis.g`.
3642    let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3643    let out = run_chunk_in_global_scope_inner(chunk);
3644    with_host(|h| h.module_scope = prev_scope);
3645    out
3646}
3647
3648fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3649    let global_env = with_host(|h| h.global_env.clone());
3650    with_host(|h| {
3651        h.frames.push(Frame {
3652            env: global_env.clone(),
3653            base_env: global_env,
3654            this_obj: None,
3655            new_target: None,
3656            home_class: None,
3657            home_static: false,
3658            home_object: None,
3659            strict: false,
3660            line: 0,
3661            owner: None,
3662            is_module: true,
3663            this_state: ThisState::Plain,
3664        })
3665    });
3666    let r = run_chunk_on(chunk);
3667    with_host(|h| {
3668        h.frames.pop();
3669    });
3670    r
3671}
3672
3673/// Run the top-level program chunk, then drain the event loop (microtasks +
3674/// timers) until quiescent — matching Node, which keeps the process alive while
3675/// pending async work remains.
3676pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3677    with_host(|h| h.module_scope = true);
3678    let r = run_chunk_on(chunk);
3679    with_host(|h| h.signal = None);
3680    if r.is_ok() {
3681        run_event_loop()?;
3682        finish_process_events()?;
3683    }
3684    r
3685}
3686
3687/// The shutdown sequence Node runs once the loop has drained on its own: fire
3688/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3689/// again and `beforeExit` fires again), then fire `exit` exactly once.
3690///
3691/// Neither event fired at all before this existed, so `process.on('exit', …)`
3692/// was a registration with no delivery — a listener whose body printed was
3693/// silently dropped, and one that set `process.exitCode` could not affect the
3694/// status. Measured on node v26.7.0,
3695/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3696///
3697/// An explicit `process.exit()` never reaches here (it leaves the process from
3698/// inside the builtin), and neither does an uncaught exception — matching
3699/// Node, where `beforeExit` is skipped on both paths.
3700fn finish_process_events() -> Result<(), String> {
3701    // Bounded: a `beforeExit` listener that re-arms work every time would spin
3702    // forever, exactly as it does in Node, but a runaway here would hang a
3703    // parity run with no output, so it is capped and then treated as drained.
3704    for _ in 0..1000 {
3705        let code = with_host(|h| h.exit_code).unwrap_or(0);
3706        if !crate::stdlib::process::emit_before_exit(code)? {
3707            break;
3708        }
3709        let more =
3710            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3711        if !more {
3712            break;
3713        }
3714        run_event_loop()?;
3715    }
3716    let code = with_host(|h| h.exit_code).unwrap_or(0);
3717    crate::stdlib::process::emit_exit_event(code)
3718}
3719
3720// ── formatting ───────────────────────────────────────────────────────────────
3721
3722/// Format a JS number exactly as `Number.prototype.toString` does for the common
3723/// range (no exponential-notation threshold handling for very large/small).
3724pub fn fmt_number(f: f64) -> String {
3725    if f.is_nan() {
3726        return "NaN".into();
3727    }
3728    if f.is_infinite() {
3729        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3730    }
3731    if f == 0.0 {
3732        // Covers -0.0 too: (-0).toString() === "0".
3733        return "0".into();
3734    }
3735    if f < 0.0 {
3736        return format!("-{}", js_number_repr(-f));
3737    }
3738    js_number_repr(f)
3739}
3740
3741/// If `k` is an array-index property key, return its numeric value. Per
3742/// ECMAScript, a String property key `P` is an array index iff
3743/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3744/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3745pub fn array_index(k: &str) -> Option<u32> {
3746    if k.is_empty() {
3747        return None;
3748    }
3749    if k == "0" {
3750        return Some(0);
3751    }
3752    // A leading '0' (other than the lone "0" above) is non-canonical.
3753    if k.as_bytes()[0] == b'0' {
3754        return None;
3755    }
3756    if !k.bytes().all(|b| b.is_ascii_digit()) {
3757        return None;
3758    }
3759    match k.parse::<u64>() {
3760        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3761        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3762        _ => None,
3763    }
3764}
3765
3766/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3767/// integer-index keys sort ascending-numeric and precede all string keys; two
3768/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3769/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3770/// they also fall into the stable-insertion-order tail.)
3771pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3772    use std::cmp::Ordering;
3773    match (array_index(a), array_index(b)) {
3774        (Some(x), Some(y)) => x.cmp(&y),
3775        (Some(_), None) => Ordering::Less,
3776        (None, Some(_)) => Ordering::Greater,
3777        (None, None) => Ordering::Equal,
3778    }
3779}
3780
3781/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3782/// place: array-index keys ascending first, then the remaining keys in their
3783/// existing (insertion) order. A no-op unless at least one index key is present,
3784/// so the overwhelmingly common all-string-key object keeps its exact order and
3785/// pays nothing. `IndexMap::sort_by` is a stable sort.
3786pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3787    if props.keys().any(|k| array_index(k).is_some()) {
3788        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3789    }
3790}
3791
3792/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3793///
3794/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3795/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3796/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3797/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3798/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3799/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3800/// `n > 21` or `n ≤ -6`.
3801fn js_number_repr(a: f64) -> String {
3802    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3803    // round-trip digits. Split it into the digit string `s` and exponent `E`.
3804    let sci = format!("{a:e}");
3805    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3806    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3807    let s: String = mant.chars().filter(|c| *c != '.').collect();
3808    let k = s.len() as i32; // number of significant digits
3809    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3810
3811    if k <= n && n <= 21 {
3812        // Integer with trailing zeros: all digits, then n−k zeros.
3813        let mut out = s;
3814        out.push_str(&"0".repeat((n - k) as usize));
3815        out
3816    } else if 0 < n && n <= 21 {
3817        // Decimal point inside the digit run: n digits, '.', the rest.
3818        format!("{}.{}", &s[..n as usize], &s[n as usize..])
3819    } else if -6 < n && n <= 0 {
3820        // Leading "0." then (−n) zeros then all digits.
3821        format!("0.{}{}", "0".repeat((-n) as usize), s)
3822    } else {
3823        // Exponential form. Exponent digit is n−1, always signed.
3824        let exp = n - 1;
3825        let sign = if exp >= 0 { '+' } else { '-' };
3826        let mag = exp.abs();
3827        if k == 1 {
3828            format!("{s}e{sign}{mag}")
3829        } else {
3830            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3831        }
3832    }
3833}
3834
3835impl JsHost {
3836    /// The `typeof` string for `v`.
3837    pub fn type_of(&self, v: &Value) -> &'static str {
3838        match v {
3839            Value::Undef => "undefined",
3840            Value::Bool(_) => "boolean",
3841            Value::Int(_) | Value::Float(_) => "number",
3842            Value::Str(_) => "string",
3843            Value::Obj(_) => match self.get(v) {
3844                Some(JsObj::Str(_)) => "string",
3845                // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3846                // target is callable, so `typeof` classifies by the target —
3847                // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3848                // is bounded: a proxy of a proxy defers again.
3849                Some(JsObj::Proxy { target, .. }) => {
3850                    let mut cur = target;
3851                    for _ in 0..100 {
3852                        match self.get(cur) {
3853                            Some(JsObj::Proxy { target: t, .. }) => cur = t,
3854                            _ => break,
3855                        }
3856                    }
3857                    if is_callable(self, cur) {
3858                        "function"
3859                    } else {
3860                        "object"
3861                    }
3862                }
3863                Some(JsObj::Func(_))
3864                | Some(JsObj::BoundMethod { .. })
3865                | Some(JsObj::BoundFunc { .. })
3866                | Some(JsObj::Class(_)) => "function",
3867                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3868                // `typeof === "function"` — EXCEPT the non-callable namespace
3869                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3870                Some(JsObj::Builtin(n)) => {
3871                    if builtin_is_callable(n) {
3872                        "function"
3873                    } else {
3874                        "object"
3875                    }
3876                }
3877                Some(JsObj::Symbol { .. }) => "symbol",
3878                Some(JsObj::BigInt(_)) => "bigint",
3879                _ => "object", // arrays, objects, null, Map/Set, generators
3880            },
3881            _ => "object",
3882        }
3883    }
3884
3885    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3886    pub fn truthy(&self, v: &Value) -> bool {
3887        match v {
3888            Value::Undef => false,
3889            Value::Bool(b) => *b,
3890            Value::Int(n) => *n != 0,
3891            Value::Float(f) => *f != 0.0 && !f.is_nan(),
3892            Value::Str(s) => !s.is_empty(),
3893            Value::Obj(_) => match self.get(v) {
3894                Some(JsObj::Str(s)) => !s.is_empty(),
3895                Some(JsObj::Null) => false,
3896                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3897                _ => true, // arrays, objects, functions
3898            },
3899            _ => true,
3900        }
3901    }
3902
3903    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3904    pub fn to_number(&self, v: &Value) -> f64 {
3905        match v {
3906            Value::Undef => f64::NAN,
3907            Value::Bool(b) => {
3908                if *b {
3909                    1.0
3910                } else {
3911                    0.0
3912                }
3913            }
3914            Value::Int(n) => *n as f64,
3915            Value::Float(f) => *f,
3916            Value::Str(s) => str_to_number(s),
3917            Value::Obj(_) => match self.get(v) {
3918                Some(JsObj::Str(s)) => str_to_number(s),
3919                Some(JsObj::Null) => 0.0,
3920                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3921                Some(JsObj::Array(items)) => {
3922                    // [] -> 0, [x] -> ToNumber(x), else NaN.
3923                    if items.is_empty() {
3924                        0.0
3925                    } else if items.len() == 1 {
3926                        self.to_number(&items[0])
3927                    } else {
3928                        f64::NAN
3929                    }
3930                }
3931                _ => f64::NAN,
3932            },
3933            _ => f64::NAN,
3934        }
3935    }
3936
3937    /// `String(v)` — the string-coercion form (raw, unquoted).
3938    pub fn str_of(&self, v: &Value) -> String {
3939        match v {
3940            Value::Undef => "undefined".into(),
3941            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3942            Value::Int(n) => n.to_string(),
3943            Value::Float(f) => fmt_number(*f),
3944            Value::Str(s) => (**s).clone(),
3945            Value::Obj(_) => match self.get(v) {
3946                Some(JsObj::Str(s)) => s.clone(),
3947                Some(JsObj::Null) => "null".into(),
3948                Some(JsObj::BigInt(b)) => b.to_string(),
3949                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3950                Some(JsObj::Array(items)) => {
3951                    // Array.prototype.toString: comma-join, null/undefined -> "".
3952                    // Guarded by the JoinStack (see `join_stack_push`) so a
3953                    // self-referential array yields "" instead of recursing until
3954                    // the native stack aborts the process.
3955                    if !join_stack_push(v) {
3956                        return String::new();
3957                    }
3958                    let parts: Vec<String> = items
3959                        .iter()
3960                        .map(|x| match x {
3961                            Value::Undef => String::new(),
3962                            _ if self.is_null(x) => String::new(),
3963                            _ => self.str_of(x),
3964                        })
3965                        .collect();
3966                    join_stack_pop();
3967                    parts.join(",")
3968                }
3969                Some(JsObj::Object(props)) => {
3970                    // A native `Buffer` stringifies to its decoded (utf-8)
3971                    // contents, matching `buf.toString()` — needed for `'' + buf`,
3972                    // template interpolation, and `data += chunk` (the pattern
3973                    // Express/body-parser use to read a request body).
3974                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3975                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3976                            Some(JsObj::Array(items)) => {
3977                                items.iter().map(|x| self.to_number(x) as u8).collect()
3978                            }
3979                            _ => Vec::new(),
3980                        };
3981                        String::from_utf8_lossy(&bytes).into_owned()
3982                    } else if let Some(s) = self.error_to_string(v) {
3983                        s
3984                    } else {
3985                        "[object Object]".into()
3986                    }
3987                }
3988                Some(JsObj::Func(f)) => {
3989                    // A function built from runtime source (`new Function`,
3990                    // `vm.compileFunction`) retains the exact text V8 synthesizes
3991                    // for it, so `Function.prototype.toString` reports what Node
3992                    // reports. Every other function slices its span out of the
3993                    // script it was parsed from; only one whose program kept no
3994                    // text (an AOT image, a `rust { }` desugared file) falls back
3995                    // to the placeholder.
3996                    if let Some(src) = self.fn_prop(v, "@@source") {
3997                        return self.str_of(&src);
3998                    }
3999                    if let Some(text) = self.func_source(f.def_id) {
4000                        return text.to_string();
4001                    }
4002                    let name = self
4003                        .funcs
4004                        .get(f.def_id)
4005                        .map(|d| d.name.clone())
4006                        .unwrap_or_default();
4007                    format!("function {name}() {{ [code] }}")
4008                }
4009                // The native-code form names the FUNCTION, not its key:
4010                // `String(Math.max)` is `function max() { [native code] }`.
4011                Some(JsObj::Builtin(n)) => {
4012                    // The `console` methods are the exception node itself makes:
4013                    // each is a wrapper, so `String(console.log)` is the
4014                    // ANONYMOUS native-code form even though `console.log.name`
4015                    // is `log`. Measured on v26.8.1.
4016                    if n.starts_with("console.") {
4017                        "function () { [native code] }".into()
4018                    } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
4019                        // An accessor half names itself `get size` / `set
4020                        // arguments`, which `builtin_name` cannot build because
4021                        // it returns a borrowed `&str`.
4022                        format!("function {accessor}() {{ [native code] }}")
4023                    } else {
4024                        format!(
4025                            "function {}() {{ [native code] }}",
4026                            crate::builtins::builtin_name(n)
4027                        )
4028                    }
4029                }
4030                // A method read off an instance names itself the same way the
4031                // prototype method it resolves to does: `String([].slice)` is
4032                // `function slice() { [native code] }`.
4033                Some(JsObj::BoundMethod { name, .. }) => {
4034                    format!("function {name}() {{ [native code] }}")
4035                }
4036                Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4037                // `Function.prototype.toString` refuses to expose a proxy's
4038                // target: V8 reports the native-code form for a proxy of ANY
4039                // callable, so `String(new Proxy(function f(){}, {}))` is
4040                // `function () { [native code] }`, not `f`'s source.
4041                Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4042                    "function () { [native code] }".into()
4043                }
4044                Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4045                    Some(text) => text.to_string(),
4046                    None => format!("class {} {{ }}", c.name),
4047                },
4048                Some(JsObj::Symbol { desc, .. }) => {
4049                    // `String(sym)` is allowed (unlike implicit coercion) and yields
4050                    // `Symbol(desc)`.
4051                    match desc {
4052                        Some(d) => format!("Symbol({d})"),
4053                        None => "Symbol()".into(),
4054                    }
4055                }
4056                _ => "[object Object]".into(),
4057            },
4058            _ => "[object Object]".into(),
4059        }
4060    }
4061
4062    /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4063    /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4064    /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4065    /// entry and showing it twice would be wrong.
4066    ///
4067    /// Only a DATA property is seen. A tag supplied by a prototype getter
4068    /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4069    /// which cannot run under the host borrow `inspect` holds — such an object
4070    /// prints without the prefix.
4071    /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4072    /// a primitive wrapper, distinguishing it from the bare primitive.
4073    fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4074        let prim = match self.get(v) {
4075            Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4076            _ => return None,
4077        };
4078        let ctor = match &prim {
4079            Value::Bool(_) => "Boolean",
4080            Value::Int(_) | Value::Float(_) => "Number",
4081            // BigInt and Symbol primitives live on the heap; their boxes are
4082            // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4083            _ => match self.get(&prim) {
4084                Some(JsObj::BigInt(_)) => "BigInt",
4085                Some(JsObj::Symbol { .. }) => "Symbol",
4086                _ => "String",
4087            },
4088        };
4089        let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4090        // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4091        // boxed characters are NOT extras — node hides the index properties of
4092        // a String wrapper, showing only what was added to it.
4093        let width = if ctor == "String" {
4094            self.str_of(&prim).chars().count()
4095        } else {
4096            0
4097        };
4098        let extras: Vec<String> = match self.get(v) {
4099            Some(JsObj::Object(p)) => p
4100                .iter()
4101                .filter(|(k, _)| {
4102                    !k.starts_with("@@")
4103                        && !k.starts_with('#')
4104                        && self.prop_attrs(v, k).enumerable
4105                        && !k.parse::<usize>().is_ok_and(|i| i < width)
4106                })
4107                .map(|(k, val)| {
4108                    format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4109                })
4110                .collect(),
4111            _ => Vec::new(),
4112        };
4113        if extras.is_empty() {
4114            return Some(head);
4115        }
4116        Some(self.render_object(&extras, &format!("{head} "), indent, st))
4117    }
4118
4119    /// The `key: value` parts for own properties a script attached to an exotic
4120    /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4121    /// prints `Map(1) { 'k' => 1, x: 5 }`.
4122    fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4123        self.fn_prop_keys(v)
4124            .into_iter()
4125            .filter(|k| {
4126                !k.starts_with("@@")
4127                    && !k.starts_with('#')
4128                    && !is_symbol_key(k)
4129                    && self.prop_attrs(v, k).enumerable
4130            })
4131            .map(|k| {
4132                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4133                format!(
4134                    "{}: {}",
4135                    fmt_key(&k),
4136                    self.inspect_lvl(&val, indent + 2, st)
4137                )
4138            })
4139            .collect()
4140    }
4141
4142    fn inspect_tag(&self, v: &Value) -> Option<String> {
4143        let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4144        if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4145            return None;
4146        }
4147        let t = lookup_chain(self, v, "@@toStringTag")?;
4148        self.as_str(&t)
4149    }
4150
4151    /// `console.log`-style rendering of a top-level argument: bare strings print
4152    /// raw; everything else uses `inspect`.
4153    pub fn console_format(&self, v: &Value) -> String {
4154        match v {
4155            Value::Str(_) => self.str_of(v),
4156            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4157            _ => self.inspect(v),
4158        }
4159    }
4160
4161    /// `util.inspect`-style rendering (nested; strings quoted).
4162    pub fn inspect(&self, v: &Value) -> String {
4163        self.inspect_lvl(v, 0, &mut InspectCycles::default())
4164    }
4165
4166    /// `util.inspect` at a given indentation level, with the cycle guard applied
4167    /// around the object cases.
4168    ///
4169    /// A value already being rendered further up the chain is a CYCLE, and Node
4170    /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4171    /// object it points back at is prefixed `<ref *N>`. Without this the walk
4172    /// only stopped when the depth limit turned the back-edge into `[Object]`,
4173    /// so `const c={a:1}; c.c=c` printed the misleading
4174    /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4175    /// `<ref *1> { a: 1, c: [Circular *1] }`.
4176    ///
4177    /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4178    /// the target's own children are being rendered — so the prefix can only be
4179    /// decided after `inspect_value` returns.
4180    /// Whether `v` renders as a LEAF — a finished string produced without
4181    /// recursing into any child.
4182    ///
4183    /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4184    /// after the early returns for the shapes that answer immediately: a bare
4185    /// Date is its ISO string, a regex is its literal, an empty container is its
4186    /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4187    /// those record a depth, so a group containing one is not pushed over the
4188    /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4189    /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4190    fn renders_without_expanding(&self, v: &Value) -> bool {
4191        let plain_props = |p: &IndexMap<String, Value>| {
4192            p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4193        };
4194        match self.get(v) {
4195            // A regex never recurses, with or without its hidden `lastIndex`.
4196            Some(JsObj::RegExp(_)) => true,
4197            Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4198            Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4199            Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4200            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4201                Some("Buffer") => inspect_custom(),
4202                // Own properties added to a Date DO get expanded after it.
4203                Some("Date") => plain_props(p),
4204                Some(_) => false,
4205                None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4206            },
4207            _ => false,
4208        }
4209    }
4210
4211    fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4212        if !matches!(v, Value::Obj(_)) {
4213            return self.inspect_value(v, indent, st);
4214        }
4215        if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4216            return format!("[Circular *{}]", st.mark(self, v));
4217        }
4218        st.seen.push(v.clone());
4219        // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4220        // it expands — not a running maximum — so after the children have been
4221        // rendered it holds the depth of the last chain below this group, which
4222        // is what `reduceToSingleString` compares. A value the depth limit
4223        // stubs out as `[Object]` is never expanded and must not count, or an
4224        // object whose deepest level was elided would break where node joins.
4225        // Only a value node actually EXPANDS advances the depth. A string,
4226        // symbol or bigint is a JS primitive that this host happens to store on
4227        // the heap, so it reaches here as `Value::Obj` where an unboxed number
4228        // returns above — and counting it as a level made any group holding one
4229        // look deeper than it was. Under `compact: 1` that is the difference
4230        // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4231        // Map across four lines, because its string KEYS were being charged a
4232        // nesting level.
4233        if indent as i64 <= inspect_indent_limit()
4234            && !is_primitive(self, v)
4235            && !self.renders_without_expanding(v)
4236        {
4237            st.deepest = indent;
4238        }
4239        let body = self.inspect_value(v, indent, st);
4240        st.seen.pop();
4241        match st.id_of(self, v) {
4242            Some(id) => format!("<ref *{id}> {body}"),
4243            None => body,
4244        }
4245    }
4246
4247    /// The rendering itself, once `inspect_lvl` has established that `v` is not
4248    /// a back-edge into an object already on the stack.
4249    fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4250        if let Some(s) = self.inspect_wrapper(v, indent, st) {
4251            return s;
4252        }
4253        match v {
4254            Value::Undef => "undefined".into(),
4255            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4256            Value::Int(n) => n.to_string(),
4257            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4258            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4259            Value::Float(f) => fmt_number(*f),
4260            Value::Str(s) => quote_str(s),
4261            Value::Obj(_) => match self.get(v) {
4262                Some(JsObj::Str(s)) => quote_str(s),
4263                Some(JsObj::Null) => "null".into(),
4264                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4265                Some(JsObj::BigInt(b)) => format!("{b}n"),
4266                // `lastIndex` is a non-enumerable own property of every regex,
4267                // so `showHidden` (and therefore `%o`) appends it:
4268                // `/x/g { [lastIndex]: 0 }`.
4269                Some(JsObj::RegExp(r)) => {
4270                    let body = format!("/{}/{}", r.source, r.flags);
4271                    if inspect_show_hidden() {
4272                        format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4273                    } else {
4274                        body
4275                    }
4276                }
4277                // `util.inspect` on node v26.7.0 renders a proxy as
4278                // `Proxy(<target>)` — the target's own rendering, wrapped. It
4279                // deliberately does NOT run the handler's traps, so this stays a
4280                // pure `&self` read like every other inspect arm.
4281                Some(JsObj::Proxy { target, .. }) => {
4282                    format!("Proxy({})", self.inspect_lvl(target, indent, st))
4283                }
4284                // `arguments` is backed by an Array but is an ordinary-shaped
4285                // exotic to util.inspect: node prints its indices as quoted
4286                // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4287                Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4288                    if indent as i64 > inspect_indent_limit() {
4289                        return "[Arguments]".into();
4290                    }
4291                    let mut inner: Vec<String> = items
4292                        .iter()
4293                        .enumerate()
4294                        .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4295                        .collect();
4296                    for k in self.fn_prop_keys(v) {
4297                        if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4298                            continue;
4299                        }
4300                        let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4301                        inner.push(format!(
4302                            "{}: {}",
4303                            fmt_key(&k),
4304                            self.inspect_lvl(&val, indent + 2, st)
4305                        ));
4306                    }
4307                    if inner.is_empty() {
4308                        return "[Arguments] {}".into();
4309                    }
4310                    self.render_object(&inner, "[Arguments] ", indent, st)
4311                }
4312                Some(JsObj::Array(items)) => {
4313                    // Own enumerable non-index string props (e.g. a `str.match(re)`
4314                    // result's `index`/`input`/`groups`, or a user-assigned
4315                    // `arr.foo`) render after the elements, as `key: value`.
4316                    let prop_keys: Vec<String> = self
4317                        .fn_prop_keys(v)
4318                        .into_iter()
4319                        .filter(|k| {
4320                            !k.starts_with("@@")
4321                                && !k.starts_with('#')
4322                                && self.prop_attrs(v, k).enumerable
4323                        })
4324                        .collect();
4325                    // An own enumerable SYMBOL-keyed property renders after the
4326                    // string keys as `Symbol(desc): value`, as it does on an
4327                    // object receiver.
4328                    // An instance of an Array SUBCLASS leads with its
4329                    // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4330                    // `getPrefix` does for any non-`Array` constructor.
4331                    let sub = match self.proto_of(v) {
4332                        Some(_) => self.ctor_name(v),
4333                        None => String::new(),
4334                    };
4335                    let base = if sub.is_empty() || sub == "Array" {
4336                        String::new()
4337                    } else {
4338                        format!("{sub}({}) ", items.len())
4339                    };
4340                    let sym_entries = self.own_symbol_entries(v);
4341                    // Under `showHidden` even an empty array has something to
4342                    // show — node prints `[ [length]: 0 ]`, not `[]`.
4343                    if items.is_empty()
4344                        && prop_keys.is_empty()
4345                        && sym_entries.is_empty()
4346                        && !inspect_show_hidden()
4347                    {
4348                        return format!("{base}[]");
4349                    }
4350                    // Node's default inspect depth is 2 (root = depth 0); deeper
4351                    // nesting collapses to `[Array]`. indent grows by 2 per level.
4352                    if indent as i64 > inspect_indent_limit() {
4353                        return "[Array]".into();
4354                    }
4355                    // `util.inspect`'s `maxArrayLength` (default 100): only the
4356                    // first 100 elements are formatted, and the rest collapse to
4357                    // a `... N more items` entry. Without the cap a 120-element
4358                    // array printed all 120 — and, because the grid column width
4359                    // is computed from what is SHOWN, every column was also one
4360                    // character wider than node's.
4361                    // A SPARSE array takes node's `formatSpecialArray` path: an
4362                    // elided run renders as `<N empty items>` rather than as the
4363                    // `undefined` it reads back as.
4364                    let (mut inner, has_tail) = if self.has_holes(v) {
4365                        self.inspect_sparse(v, items, indent, st)
4366                    } else {
4367                        let shown = items.len().min(inspect_max_array_length());
4368                        let mut inner: Vec<String> = items[..shown]
4369                            .iter()
4370                            .map(|x| self.inspect_lvl(x, indent + 2, st))
4371                            .collect();
4372                        let remaining = items.len() - shown;
4373                        if remaining > 0 {
4374                            let unit = if remaining == 1 { "item" } else { "items" };
4375                            inner.push(format!("... {remaining} more {unit}"));
4376                        }
4377                        (inner, remaining > 0)
4378                    };
4379                    // `showHidden` exposes the non-enumerable `length`, which an
4380                    // array always has. It sorts BEFORE any own property node
4381                    // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4382                    // rather than an element, it also turns the column grid off —
4383                    // which is why a ten-element array under `showHidden` prints
4384                    // on one line rather than as a grid.
4385                    let show_hidden = inspect_show_hidden();
4386                    if show_hidden {
4387                        inner.push(format!("[length]: {}", items.len()));
4388                    }
4389                    let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4390                    for k in &prop_keys {
4391                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4392                        inner.push(format!(
4393                            "{}: {}",
4394                            fmt_key(k),
4395                            self.inspect_lvl(&val, indent + 2, st)
4396                        ));
4397                    }
4398                    for (k, val) in &sym_entries {
4399                        let label = match self.symbol_of_key(k) {
4400                            Some(s) => self.inspect(&s),
4401                            None => continue,
4402                        };
4403                        inner.push(format!(
4404                            "{label}: {}",
4405                            self.inspect_lvl(val, indent + 2, st)
4406                        ));
4407                    }
4408                    self.render_array(
4409                        &inner,
4410                        items,
4411                        indent,
4412                        ArrayLayout {
4413                            has_props,
4414                            has_tail,
4415                            base: &base,
4416                        },
4417                        st,
4418                    )
4419                }
4420                // `URLSearchParams` renders its pairs, not its slots:
4421                // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4422                // which is why it is a pair list rather than a Map rendering.
4423                Some(JsObj::Object(props))
4424                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4425                        == Some("URLSearchParams") =>
4426                {
4427                    let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4428                        Some(JsObj::Array(items)) => items.clone(),
4429                        _ => Vec::new(),
4430                    };
4431                    if pairs.is_empty() {
4432                        return "URLSearchParams {}".into();
4433                    }
4434                    let inner: Vec<String> = pairs
4435                        .iter()
4436                        .filter_map(|kv| match self.get(kv) {
4437                            Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4438                                "{} => {}",
4439                                self.inspect_lvl(&p[0], indent + 2, st),
4440                                self.inspect_lvl(&p[1], indent + 2, st)
4441                            )),
4442                            _ => None,
4443                        })
4444                        .collect();
4445                    self.render_object(&inner, "URLSearchParams ", indent, st)
4446                }
4447                // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4448                // constructor and length, then the elements laid out exactly as
4449                // an array's. Without this it fell through to the generic object
4450                // arm and printed the `{ length, byteLength, byteOffset,
4451                // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4452                // which is the whole reason anyone logs one.
4453                Some(JsObj::Object(props))
4454                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4455                        == Some("TypedArray") =>
4456                {
4457                    let kind = props
4458                        .get("@@kind")
4459                        .map(|k| self.str_of(k))
4460                        .unwrap_or_else(|| "TypedArray".into());
4461                    // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4462                    // which this shared borrow cannot allocate as values.
4463                    let elems = crate::stdlib::typedarray::elems_display(self, v);
4464                    // The grid layout sizes its columns from the VALUES; a
4465                    // 64-bit view's come back as `undefined` (no allocation is
4466                    // possible here), which only affects column padding.
4467                    let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4468                    let base = format!("{kind}({}) ", elems.len());
4469                    if indent as i64 > inspect_indent_limit() {
4470                        return format!("[{kind}]");
4471                    }
4472                    let shown = elems.len().min(inspect_max_array_length());
4473                    let mut inner: Vec<String> = elems[..shown].to_vec();
4474                    let remaining = elems.len() - shown;
4475                    if remaining > 0 {
4476                        let unit = if remaining == 1 { "item" } else { "items" };
4477                        inner.push(format!("... {remaining} more {unit}"));
4478                    }
4479                    // A view's whole identity — its element width, its window
4480                    // onto the backing store, and the store itself — is
4481                    // non-enumerable, so `showHidden` is the only way to see it.
4482                    // `util.format('%o', view)` goes through here, since `%o`
4483                    // implies `showHidden`.
4484                    let show_hidden = inspect_show_hidden();
4485                    if show_hidden {
4486                        let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4487                        let byte_offset = props
4488                            .get("byteOffset")
4489                            .map(|x| self.to_number(x))
4490                            .unwrap_or(0.0);
4491                        inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4492                        inner.push(format!("[length]: {}", elems.len()));
4493                        inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4494                        inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4495                        // An ArrayBuffer reached AS a view's backing store is
4496                        // rendered by node WITHOUT its contents — just
4497                        // `ArrayBuffer { [byteLength]: N }` — even though the
4498                        // same buffer inspected on its own leads with
4499                        // `[Uint8Contents]`. Recursing through the normal
4500                        // ArrayBuffer branch therefore printed the bytes twice,
4501                        // once as the view's elements and again as the store's.
4502                        let buf_len = props
4503                            .get("@@buffer")
4504                            .and_then(|b| self.get(b))
4505                            .and_then(|o| match o {
4506                                JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4507                                _ => None,
4508                            })
4509                            .and_then(|b| {
4510                                self.get(&b).map(|o| match o {
4511                                    JsObj::Array(items) => items.len(),
4512                                    _ => 0,
4513                                })
4514                            })
4515                            .unwrap_or(0);
4516                        inner.push(format!(
4517                            "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4518                        ));
4519                    }
4520                    self.render_array(
4521                        &inner,
4522                        &vals,
4523                        indent,
4524                        ArrayLayout {
4525                            has_props: show_hidden,
4526                            has_tail: remaining > 0,
4527                            base: &base,
4528                        },
4529                        st,
4530                    )
4531                }
4532                // An `ArrayBuffer` renders its CONTENTS, which is the only way
4533                // to see them — it exposes no indices of its own:
4534                // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4535                Some(JsObj::Object(props))
4536                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4537                        == Some("ArrayBuffer") =>
4538                {
4539                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4540                        Some(JsObj::Array(items)) => {
4541                            items.iter().map(|x| self.to_number(x) as u8).collect()
4542                        }
4543                        _ => Vec::new(),
4544                    };
4545                    let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4546                    let mut parts = vec![
4547                        format!("[Uint8Contents]: <{}>", hex.join(" ")),
4548                        format!("[byteLength]: {}", bytes.len()),
4549                    ];
4550                    if props.contains_key("@@maxByteLength") {
4551                        let max = props
4552                            .get("@@maxByteLength")
4553                            .map(|m| self.to_number(m))
4554                            .unwrap_or(0.0);
4555                        parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4556                    }
4557                    self.render_object(&parts, "ArrayBuffer ", indent, st)
4558                }
4559                // A `Date` renders as its ISO-8601 form. Its time value lives in
4560                // the internal `@@ms` slot, which the generic object branch below
4561                // does not show, so without this arm every Date printed as `{}` —
4562                // including through `console.log(d)`, inside arrays, objects and
4563                // Maps, and in an `assert` diff.
4564                Some(JsObj::Object(props))
4565                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4566                {
4567                    let base = crate::stdlib::date::inspect_with_host(self, v);
4568                    // Own properties added to a Date follow the date itself, the
4569                    // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4570                    let extra = self.side_table_parts(v, indent, st);
4571                    let mut inner: Vec<String> = props
4572                        .iter()
4573                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4574                        .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4575                        .collect();
4576                    inner.extend(extra);
4577                    if inner.is_empty() {
4578                        return base;
4579                    }
4580                    self.render_object(&inner, &format!("{base} "), indent, st)
4581                }
4582                // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4583                // at 50 with a `... N more byte(s)` tail, exactly as
4584                // `util.inspect` does. Without this a `console.log(buf)` (the
4585                // single most common thing anyone does with a Buffer) printed
4586                // the internal `{ length, byteLength, … }` bookkeeping.
4587                Some(JsObj::Object(props))
4588                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4589                        == Some("Buffer") =>
4590                {
4591                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4592                        Some(JsObj::Array(items)) => {
4593                            items.iter().map(|x| self.to_number(x) as u8).collect()
4594                        }
4595                        _ => Vec::new(),
4596                    };
4597                    // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4598                    // the shape of the object. Under `customInspect: false` node
4599                    // does not call that hook and falls back to the generic
4600                    // byte-view rendering — which is what an `assert` diff shows,
4601                    // since assert inspects with the hook disabled so that a
4602                    // failure names the differing BYTE rather than two opaque hex
4603                    // blobs. The constructor is `Buffer` while the brand is still
4604                    // `Uint8Array`, so node prints both.
4605                    if !inspect_custom() {
4606                        let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4607                        if indent as i64 > inspect_indent_limit() {
4608                            return "[Buffer [Uint8Array]]".into();
4609                        }
4610                        let shown = bytes.len().min(inspect_max_array_length());
4611                        let mut inner: Vec<String> =
4612                            bytes[..shown].iter().map(|b| b.to_string()).collect();
4613                        let vals: Vec<Value> = bytes[..shown]
4614                            .iter()
4615                            .map(|b| Value::Float(*b as f64))
4616                            .collect();
4617                        let remaining = bytes.len() - shown;
4618                        if remaining > 0 {
4619                            let unit = if remaining == 1 { "item" } else { "items" };
4620                            inner.push(format!("... {remaining} more {unit}"));
4621                        }
4622                        return self.render_array(
4623                            &inner,
4624                            &vals,
4625                            indent,
4626                            ArrayLayout {
4627                                has_props: false,
4628                                has_tail: remaining > 0,
4629                                base: &base,
4630                            },
4631                            st,
4632                        );
4633                    }
4634                    const MAX: usize = 50;
4635                    let shown: Vec<String> =
4636                        bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4637                    let mut out = format!("<Buffer {}", shown.join(" "));
4638                    if bytes.len() > MAX {
4639                        let more = bytes.len() - MAX;
4640                        let unit = if more == 1 { "byte" } else { "bytes" };
4641                        out.push_str(&format!(" ... {more} more {unit}"));
4642                    }
4643                    out.push('>');
4644                    out
4645                }
4646                // An Error inspects as its `.stack` — never as an object literal
4647                // exposing the internal `message`/`stack` slots. Any own property
4648                // a script added beyond those follows in braces, as V8 renders
4649                // it: `Error: x\n    at … { code: 'C' }`.
4650                Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4651                    let mut stack = lookup_chain(self, v, "stack")
4652                        .map(|s| self.str_of(&s))
4653                        .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4654                    // A `DOMException` prints its CLASS and then its name —
4655                    // `DOMException [AbortError]: m` — where a plain error
4656                    // prints only its stack head.
4657                    if let Some(JsObj::Object(p)) = self.get(v) {
4658                        if let Some(n) = p.get("@@domName") {
4659                            let name = self.str_of(n);
4660                            stack = format!(
4661                                "DOMException [{name}]{}",
4662                                stack.strip_prefix(&name).unwrap_or(&stack)
4663                            );
4664                        }
4665                    }
4666                    let extra: Vec<String> = self
4667                        .own_enum_key_names(v)
4668                        .into_iter()
4669                        .filter(|k| k != "name")
4670                        .map(|k| {
4671                            let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4672                                Some(JsObj::Object(p)) => {
4673                                    p.get(&k).cloned().unwrap_or(Value::Undef)
4674                                }
4675                                _ => Value::Undef,
4676                            });
4677                            format!(
4678                                "{}: {}",
4679                                fmt_key(&k),
4680                                self.inspect_lvl(&val, indent + 2, st)
4681                            )
4682                        })
4683                        .collect();
4684                    if extra.is_empty() {
4685                        stack
4686                    } else {
4687                        format!("{stack} {{ {} }}", extra.join(", "))
4688                    }
4689                }
4690                Some(JsObj::Object(props)) => {
4691                    // Instances print with their constructor name as a prefix
4692                    // (`C { x: 1 }`); plain objects have none; a null-prototype
4693                    // object (e.g. an `Object.groupBy` result) is tagged
4694                    // `[Object: null prototype]`.
4695                    let ctor = match self.ctor_name(v) {
4696                        n if n.is_empty() => "Object".to_string(),
4697                        n => n,
4698                    };
4699                    let plain_prefix = if ctor == "Object" {
4700                        String::new()
4701                    } else {
4702                        format!("{ctor} ")
4703                    };
4704                    let prefix = if self.inspects_null_proto(v) {
4705                        "[Object: null prototype] ".to_string()
4706                    } else {
4707                        // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4708                        match self.inspect_tag(v) {
4709                            Some(t) if t != ctor => format!("{ctor} [{t}] "),
4710                            _ => plain_prefix.clone(),
4711                        }
4712                    };
4713                    // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4714                    // private class fields; a real symbol-keyed own property is a
4715                    // visible one and renders as `Symbol(desc): value`.
4716                    // An own ACCESSOR has no value to print: node shows the
4717                    // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4718                    // place. It is found through the `@@ord:` marker the
4719                    // property map holds for it, which is also what puts it in
4720                    // declaration order among the data properties. Without this
4721                    // an accessor rendered as nothing at all — `{ get z(){} }`
4722                    // printed `{}`.
4723                    let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4724                        .iter()
4725                        .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4726                            Some(real) => {
4727                                let attrs = self.prop_attrs(v, real);
4728                                let label = match self.own_accessor(v, real)? {
4729                                    (Some(_), Some(_)) => "[Getter/Setter]",
4730                                    (Some(_), None) => "[Getter]",
4731                                    (None, Some(_)) => "[Setter]",
4732                                    (None, None) => return None,
4733                                };
4734                                attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4735                            }
4736                            // Only an ENUMERABLE own property is shown, as node
4737                            // does: a native instance keeps bookkeeping (a
4738                            // `URLSearchParams`'s `size`) as a hidden own slot,
4739                            // and printing it would report a spec getter as data.
4740                            None if !k.starts_with("@@")
4741                                && !k.starts_with('#')
4742                                && self.prop_attrs(v, k).enumerable =>
4743                            {
4744                                Some((fmt_key(k), Ok(val)))
4745                            }
4746                            None => None,
4747                        })
4748                        .collect();
4749                    shown.extend(props.iter().filter_map(|(k, val)| {
4750                        let sym = self.symbol_of_key(k)?;
4751                        self.prop_attrs(v, k)
4752                            .enumerable
4753                            .then(|| (self.inspect(&sym), Ok(val)))
4754                    }));
4755                    if shown.is_empty() {
4756                        return format!("{prefix}{{}}");
4757                    }
4758                    // Depth limit (Node default 2): deeper objects collapse to
4759                    // `[Object]` (or `[ClassName]` for a named instance).
4760                    if indent as i64 > inspect_indent_limit() {
4761                        return if self.inspects_null_proto(v) {
4762                            // Already bracketed (`[Object: null prototype]`).
4763                            prefix.trim_end().to_string()
4764                        } else if plain_prefix.is_empty() {
4765                            "[Object]".into()
4766                        } else {
4767                            format!("[{}]", plain_prefix.trim_end())
4768                        };
4769                    }
4770                    let inner: Vec<String> = shown
4771                        .iter()
4772                        .map(|(k, val)| match val {
4773                            Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4774                            Err(label) => format!("{k}: {label}"),
4775                        })
4776                        .collect();
4777                    self.render_object(&inner, &prefix, indent, st)
4778                }
4779                Some(JsObj::Symbol { desc, .. }) => match desc {
4780                    Some(d) => format!("Symbol({d})"),
4781                    None => "Symbol()".into(),
4782                },
4783                Some(JsObj::Class(c)) => {
4784                    let base = if c.parent.is_some() {
4785                        let pname = c
4786                            .parent
4787                            .as_ref()
4788                            .map(|p| self.callable_name(p))
4789                            .unwrap_or_default();
4790                        format!("[class {} extends {}]", c.name, pname)
4791                    } else {
4792                        format!("[class {}]", c.name)
4793                    };
4794                    self.with_callable_props(v, base, indent, st)
4795                }
4796                // A Map/Set renders its members at the NEXT nesting level, and
4797                // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4798                // array collapses to `[Array]`. Both used to recurse through
4799                // `inspect`, which restarts at indent 0, so the depth gate never
4800                // fired: nesting printed one level too deep at every depth
4801                // (measured on node v26.7.0, four nested Maps print
4802                // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4803                // and a SELF-referential Map or Set recursed forever and aborted
4804                // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4805                // died with `fatal runtime error: stack overflow`, which no
4806                // `try`/`catch` can see. An empty one still prints in full at any
4807                // depth, as `[]`/`{}` do.
4808                // A WEAK collection never shows its contents: node prints
4809                // `WeakMap { <items unknown> }` whether it holds anything or
4810                // not, because the entries are not enumerable by design.
4811                Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4812                Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4813                Some(JsObj::Map { entries, .. }) => {
4814                    let extra = self.side_table_parts(v, indent, st);
4815                    if entries.is_empty() && extra.is_empty() {
4816                        return "Map(0) {}".into();
4817                    }
4818                    if indent as i64 > inspect_indent_limit() {
4819                        return "[Map]".into();
4820                    }
4821                    let mut inner: Vec<String> = entries
4822                        .values()
4823                        .map(|(k, val)| {
4824                            // Sequenced, not nested in one `format!`: both arms
4825                            // need the same `&mut` cycle state.
4826                            let ks = self.inspect_lvl(k, indent + 2, st);
4827                            let vs = self.inspect_lvl(val, indent + 2, st);
4828                            format!("{ks} => {vs}")
4829                        })
4830                        .collect();
4831                    inner.extend(extra);
4832                    // Laid out by the SAME routine as a plain object, not joined
4833                    // onto one line unconditionally. `Map`/`Set` were the only
4834                    // containers that never consulted `breakLength` or `compact`,
4835                    // so every collection wide enough to wrap printed as one long
4836                    // line: node breaks a seven-member Set of ten-character
4837                    // strings across seven lines, and `util.inspect(m, {compact:
4838                    // false})` — which assert's own diff renderer depends on —
4839                    // could not break a Map at all. Node builds these through
4840                    // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4841                    // is this `prefix` (the trailing space is the brace gap).
4842                    let prefix = format!("Map({}) ", entries.len());
4843                    self.render_object(&inner, &prefix, indent, st)
4844                }
4845                Some(JsObj::Set { entries, .. }) => {
4846                    let extra = self.side_table_parts(v, indent, st);
4847                    if entries.is_empty() && extra.is_empty() {
4848                        return "Set(0) {}".into();
4849                    }
4850                    if indent as i64 > inspect_indent_limit() {
4851                        return "[Set]".into();
4852                    }
4853                    let mut inner: Vec<String> = entries
4854                        .values()
4855                        .map(|v| self.inspect_lvl(v, indent + 2, st))
4856                        .collect();
4857                    inner.extend(extra);
4858                    // Same layout routine as a Map (see above). Note node does
4859                    // NOT column-group a wide Set the way it grids an array:
4860                    // `groupArrayElements` is reached only from the list
4861                    // formatter, so a 30-member Set is thirty lines.
4862                    let prefix = format!("Set({}) ", entries.len());
4863                    self.render_object(&inner, &prefix, indent, st)
4864                }
4865                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4866                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4867                    Some(c) => match c.state {
4868                        PromiseState::Pending => "Promise { <pending> }".into(),
4869                        PromiseState::Fulfilled => {
4870                            format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4871                        }
4872                        PromiseState::Rejected => {
4873                            format!(
4874                                "Promise {{ <rejected> {} }}",
4875                                self.inspect_lvl(&c.value, 0, st)
4876                            )
4877                        }
4878                    },
4879                    None => "Promise { <pending> }".into(),
4880                },
4881                Some(JsObj::Func(f)) => {
4882                    // `callable_name`, not the FuncDef name: an anonymous
4883                    // function expression gets its name by inference from the
4884                    // binding it initialises (`const f = function(){}`), and
4885                    // that lands as an own `name` property.
4886                    let name = self.callable_name(v);
4887                    // util.inspect labels a function by its kind, the same
4888                    // string V8 gives it as `Symbol.toStringTag`:
4889                    // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4890                    let kind = match self.funcs.get(f.def_id) {
4891                        Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4892                        Some(d) if d.is_generator => "GeneratorFunction",
4893                        Some(d) if d.is_async => "AsyncFunction",
4894                        _ => "Function",
4895                    };
4896                    let base = if name.is_empty() {
4897                        format!("[{kind} (anonymous)]")
4898                    } else {
4899                        format!("[{kind}: {name}]")
4900                    };
4901                    self.with_callable_props(v, base, indent, st)
4902                }
4903                Some(JsObj::Builtin(n)) => {
4904                    // A namespace object is not a function and must not be
4905                    // printed as one. The three ECMAScript namespaces carry a
4906                    // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4907                    // their members are all non-enumerable, so the braces really
4908                    // are empty. A `require()`d module namespace has no tag and
4909                    // node prints its members, which cannot be rendered here —
4910                    // formatting a member means allocating its value, and this
4911                    // runs under the host borrow.
4912                    if !builtin_is_callable(n) {
4913                        match crate::builtins::well_known_tag(self, v) {
4914                            Some(tag) => format!("Object [{tag}] {{}}"),
4915                            // `Set.prototype` inspects under the CONSTRUCTOR's
4916                            // name, not the key: node prints `Object [Set] {}`.
4917                            None => {
4918                                format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4919                            }
4920                        }
4921                    } else {
4922                        format!("[Function: {}]", crate::builtins::builtin_name(n))
4923                    }
4924                }
4925                // A bound method is not anonymous: it is the prototype method it
4926                // resolves to, so `console.log(new Uint8Array(1).set)` reports
4927                // `[Function: set]`.
4928                Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4929                Some(JsObj::BoundFunc { target, .. }) => {
4930                    let n = self.callable_name(target);
4931                    if n.is_empty() {
4932                        "[Function: bound ]".into()
4933                    } else {
4934                        format!("[Function: bound {n}]")
4935                    }
4936                }
4937                _ => "undefined".into(),
4938            },
4939            _ => "undefined".into(),
4940        }
4941    }
4942
4943    /// Append a callable's own enumerable properties to its `[Function: f]` /
4944    /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4945    /// A callable with none renders as the bare base.
4946    fn with_callable_props(
4947        &self,
4948        v: &Value,
4949        base: String,
4950        indent: usize,
4951        st: &mut InspectCycles,
4952    ) -> String {
4953        let mut inner: Vec<String> = self
4954            .own_enum_key_names(v)
4955            .into_iter()
4956            .map(|k| {
4957                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4958                format!(
4959                    "{}: {}",
4960                    fmt_key(&k),
4961                    self.inspect_lvl(&val, indent + 2, st)
4962                )
4963            })
4964            .collect();
4965        for (k, val) in self.own_symbol_entries(v) {
4966            if let Some(sym) = self.symbol_of_key(&k) {
4967                inner.push(format!(
4968                    "{}: {}",
4969                    self.inspect(&sym),
4970                    self.inspect_lvl(&val, indent + 2, st)
4971                ));
4972            }
4973        }
4974        if inner.is_empty() {
4975            return base;
4976        }
4977        self.render_object(&inner, &format!("{base} "), indent, st)
4978    }
4979
4980    /// Render a non-empty array's already-formatted element strings, applying
4981    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4982    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4983    /// `values` is the raw element list (drives numeric right-alignment); `indent`
4984    /// is the array's own indentation level.
4985    fn render_array(
4986        &self,
4987        output: &[String],
4988        values: &[Value],
4989        indent: usize,
4990        opts: ArrayLayout<'_>,
4991        st: &InspectCycles,
4992    ) -> String {
4993        let ArrayLayout {
4994            has_props,
4995            has_tail,
4996            base,
4997        } = opts;
4998        // Group array elements together if the array has more than six entries.
4999        // Arrays carrying extra own props (`index`/`input`/… on a match result)
5000        // are never grid-grouped — Node lays those out plainly.
5001        // `compact: false` (held as 0) also turns the GRID off, not just the
5002        // single-line join. Node reaches `groupArrayElements` only under
5003        // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
5004        // element per line however many there are; without this gate a 30-element
5005        // array still came back column-aligned in three rows, which is the form
5006        // assert's diff renderer splits on — every array diff would have been
5007        // computed over grid rows instead of elements.
5008        let entries = output.len();
5009        let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
5010            group_array_elements(self, output, values, indent, has_tail)
5011        } else {
5012            (output.to_vec(), false)
5013        };
5014        // A typed array prints its constructor and length ahead of the brackets
5015        // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
5016        // break-length seed, so a long tag wraps the list one entry sooner.
5017        if output.is_empty() {
5018            return format!("{base}[]");
5019        }
5020        // If no grouping happened, try to line everything up on a single line.
5021        if !grouped {
5022            // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5023            let start = output.len() + indent + 1 + base.chars().count() + 10;
5024            if self.may_compact(indent, st) && is_below_break_length(output, start) {
5025                return format!("{base}[ {} ]", output.join(", "));
5026            }
5027        }
5028        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5029        let pad = " ".repeat(indent);
5030        let sep = format!(",\n{pad}  ");
5031        format!("{base}[\n{pad}  {}\n{pad}]", lines.join(&sep))
5032    }
5033
5034    /// Render a non-empty object's already-formatted `key: value` strings with
5035    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5036    /// else one property per line indented by `indent + 2`. `prefix` is the
5037    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5038    /// Mirrors `render_array`'s break decision, including the `compact` depth
5039    /// gate.
5040    /// Whether a group at `indent` may be joined onto one line.
5041    ///
5042    /// Node's `reduceToSingleString`: only while the subtree below this group is
5043    /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5044    /// nothing qualifies and every group breaks.
5045    fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5046        let compact = inspect_compact();
5047        if compact < 1 {
5048            return false;
5049        }
5050        // Levels, not columns: the indent advances by two per level.
5051        let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5052        (depth_below as i64) < compact
5053    }
5054
5055    fn render_object(
5056        &self,
5057        output: &[String],
5058        prefix: &str,
5059        indent: usize,
5060        st: &InspectCycles,
5061    ) -> String {
5062        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5063        // For a tagged object Node folds the tag into `braces[0]` (e.g.
5064        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5065        // prefix (which carries the trailing space) plus the `{`.
5066        // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5067        // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5068        // the object shape), which is observably different from sorting keys
5069        // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5070        // `assert`'s diff renderer depends on this: without it two objects
5071        // carrying the same properties in a different insertion order diffed as
5072        // a wholesale rewrite of every line instead of as equal.
5073        let sorted_output;
5074        let output = if inspect_sorted() {
5075            let mut v = output.to_vec();
5076            v.sort();
5077            sorted_output = v;
5078            &sorted_output[..]
5079        } else {
5080            output
5081        };
5082        let braces0 = prefix.chars().count() + 1;
5083        let start = output.len() + indent + braces0 + 10;
5084        if self.may_compact(indent, st) && is_below_break_length(output, start) {
5085            return format!("{prefix}{{ {} }}", output.join(", "));
5086        }
5087        let pad = " ".repeat(indent);
5088        let sep = format!(",\n{pad}  ");
5089        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
5090    }
5091
5092    /// The `.name` of any callable (function/class/builtin/bound).
5093    pub fn callable_name(&self, v: &Value) -> String {
5094        // A user-set `.name` own property wins.
5095        if let Some(n) = self.fn_prop(v, "name") {
5096            return self.str_of(&n);
5097        }
5098        match self.get(v) {
5099            Some(JsObj::Func(f)) => self
5100                .funcs
5101                .get(f.def_id)
5102                .map(|d| d.name.clone())
5103                .unwrap_or_default(),
5104            Some(JsObj::Class(c)) => c.name.clone(),
5105            // Not the whole key: a builtin's `.name` is its last segment, and a
5106            // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5107            // `.` at all, so this reported the internal spelling verbatim and
5108            // `console.log(Uint8Array.prototype.set)` printed
5109            // `[Function: @proto:TypedArray:set]`.
5110            Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5111            Some(JsObj::BoundFunc { target, .. }) => {
5112                format!("bound {}", self.callable_name(target))
5113            }
5114            Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5115            _ => String::new(),
5116        }
5117    }
5118
5119    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5120
5121    /// Strict equality (`===`): same type and same value, no coercion.
5122    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5123        match (a, b) {
5124            (Value::Undef, Value::Undef) => true,
5125            (Value::Bool(x), Value::Bool(y)) => x == y,
5126            (Value::Str(x), Value::Str(y)) => x == y,
5127            _ => {
5128                // Numbers (NaN !== NaN, +0 === -0).
5129                let an = matches!(a, Value::Int(_) | Value::Float(_));
5130                let bn = matches!(b, Value::Int(_) | Value::Float(_));
5131                if an && bn {
5132                    let x = self.to_number(a);
5133                    let y = self.to_number(b);
5134                    return x == y;
5135                }
5136                // BigInt === BigInt compares by value (each literal is a distinct
5137                // heap cell, so reference identity would be wrong). BigInt is never
5138                // `===` a Number (different types).
5139                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5140                    return x == y;
5141                }
5142                // Heap values.
5143                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5144                    return sa == sb;
5145                }
5146                let na = self.is_null(a);
5147                let nb = self.is_null(b);
5148                if na || nb {
5149                    return na && nb;
5150                }
5151                // A builtin namespace/constructor/prototype is a SINGLETON in JS
5152                // (`Math === Math`, `Array.prototype === Array.prototype`), but
5153                // every bare reference here allocates a fresh handle, so compare
5154                // those by name rather than by heap index.
5155                if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5156                    (self.get(a), self.get(b))
5157                {
5158                    return builtin_identity(x) == builtin_identity(y);
5159                }
5160                // Reference identity for arrays/objects/functions.
5161                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5162            }
5163        }
5164    }
5165
5166    /// Whether `v` is `null` or `undefined`.
5167    pub fn is_nullish(&self, v: &Value) -> bool {
5168        matches!(v, Value::Undef) || self.is_null(v)
5169    }
5170
5171    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5172    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5173    /// `"null"`, or `"object"` (array / plain object / function).
5174    fn js_type(&self, v: &Value) -> &'static str {
5175        match v {
5176            Value::Undef => "undefined",
5177            Value::Bool(_) => "boolean",
5178            Value::Int(_) | Value::Float(_) => "number",
5179            Value::Str(_) => "string",
5180            Value::Obj(_) => match self.get(v) {
5181                Some(JsObj::Str(_)) => "string",
5182                Some(JsObj::Null) => "null",
5183                Some(JsObj::BigInt(_)) => "bigint",
5184                _ => "object",
5185            },
5186            _ => "object",
5187        }
5188    }
5189
5190    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5191    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5192    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5193    /// `[0] == ""` is `false` — never a number coercion of the object.
5194    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5195        // Same type: identical to `===` (number==number, string==string, etc.).
5196        if self.strict_eq(a, b) {
5197            return true;
5198        }
5199        let ta = self.js_type(a);
5200        let tb = self.js_type(b);
5201        // null and undefined are loosely equal only to each other.
5202        if self.is_nullish(a) || self.is_nullish(b) {
5203            return self.is_nullish(a) && self.is_nullish(b);
5204        }
5205        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5206        // values (both-BigInt was already settled by the `strict_eq` above).
5207        if ta == "bigint" || tb == "bigint" {
5208            return self.bigint_loose_eq(a, b);
5209        }
5210        if ta == tb {
5211            // Same type but not strict-equal (and not nullish) ⇒ not equal.
5212            return false;
5213        }
5214        // number ⇄ string: compare as numbers.
5215        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5216            return self.to_number(a) == self.to_number(b);
5217        }
5218        // boolean side coerces to number, then recompares.
5219        if ta == "boolean" {
5220            return self.loose_eq(&Value::Float(self.to_number(a)), b);
5221        }
5222        if tb == "boolean" {
5223            return self.loose_eq(a, &Value::Float(self.to_number(b)));
5224        }
5225        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5226        // then recompare as string==string or number==string.
5227        if ta == "object" && (tb == "number" || tb == "string") {
5228            let pa = self.str_of(a);
5229            return if tb == "string" {
5230                pa == self.str_of(b)
5231            } else {
5232                str_to_number(&pa) == self.to_number(b)
5233            };
5234        }
5235        if tb == "object" && (ta == "number" || ta == "string") {
5236            let pb = self.str_of(b);
5237            return if ta == "string" {
5238                self.str_of(a) == pb
5239            } else {
5240                self.to_number(a) == str_to_number(&pb)
5241            };
5242        }
5243        false
5244    }
5245
5246    /// The numeric-hook arithmetic/relational fallback for non-native operands
5247    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5248    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5249        use NumOp::*;
5250        match op {
5251            Add => {
5252                // `+`: if either operand is a string, concatenate string forms;
5253                // otherwise numeric addition.
5254                let a_str = self.prefers_string(a);
5255                let b_str = self.prefers_string(b);
5256                if a_str || b_str {
5257                    // String concatenation wins even with a bigint operand
5258                    // (`1n + "x"` → `"1x"`).
5259                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
5260                    Ok(self.new_str(s))
5261                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5262                    self.bigint_arith(op, a, b)
5263                } else {
5264                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5265                }
5266            }
5267            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5268                self.bigint_arith(op, a, b)
5269            }
5270            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5271            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5272            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5273            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5274            Pow => Ok(Value::Float(crate::builtins::js_pow(
5275                self.to_number(a),
5276                self.to_number(b),
5277            ))),
5278            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5279            Neg => Ok(Value::Float(-self.to_number(a))),
5280            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5281            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5282            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5283        }
5284    }
5285
5286    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5287    /// which drives `+` toward concatenation. Primitive strings qualify, and so
5288    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5289    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5290    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5291    fn prefers_string(&self, v: &Value) -> bool {
5292        match v {
5293            Value::Str(_) => true,
5294            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5295            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5296            // string primitive either.
5297            Value::Obj(_) => !matches!(
5298                self.get(v),
5299                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5300            ),
5301            _ => false,
5302        }
5303    }
5304
5305    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5306    /// lexicographic, otherwise numeric (NaN yields false).
5307    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5308        use std::cmp::Ordering;
5309        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5310            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5311            x.cmp(&y)
5312        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5313            // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5314            // order once an astral character meets a BMP one — see `utf16`.
5315            crate::utf16::cmp_units(&x, &y)
5316        } else {
5317            let x = self.to_number(a);
5318            let y = self.to_number(b);
5319            match x.partial_cmp(&y) {
5320                Some(o) => o,
5321                None => return false, // NaN operand
5322            }
5323        };
5324        match op {
5325            NumOp::Lt => ord == Ordering::Less,
5326            NumOp::Le => ord != Ordering::Greater,
5327            NumOp::Gt => ord == Ordering::Greater,
5328            NumOp::Ge => ord != Ordering::Less,
5329            _ => false,
5330        }
5331    }
5332
5333    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5334    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5335    /// BigInt with a Number throws, matching Node).
5336    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5337        if self.is_bigint_val(a) || self.is_bigint_val(b) {
5338            return self.bigint_bitwise(tag, a, b);
5339        }
5340        let x = to_int32(self.to_number(a));
5341        let y = to_int32(self.to_number(b));
5342        let r: i64 = match tag {
5343            binop::BITAND => (x & y) as i64,
5344            binop::BITOR => (x | y) as i64,
5345            binop::BITXOR => (x ^ y) as i64,
5346            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5347            binop::SHR => (x >> ((y as u32) & 31)) as i64,
5348            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5349            _ => 0,
5350        };
5351        Ok(Value::Float(r as f64))
5352    }
5353
5354    // ── BigInt operations ────────────────────────────────────────────────────
5355    /// Whether `v` is a heap `BigInt`.
5356    pub fn is_bigint_val(&self, v: &Value) -> bool {
5357        matches!(self.get(v), Some(JsObj::BigInt(_)))
5358    }
5359    /// The `BigInt` value of `v` (a heap bigint), else `None`.
5360    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5361        match self.get(v) {
5362            Some(JsObj::BigInt(b)) => Some(b.clone()),
5363            _ => None,
5364        }
5365    }
5366    /// Allocate a heap `BigInt`.
5367    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5368        self.alloc(JsObj::BigInt(b))
5369    }
5370
5371    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5372    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5373    /// `TypeError` (a string operand is handled as concatenation before we get
5374    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5375    /// exponent.
5376    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5377        use num_traits::{Signed, Zero};
5378        use NumOp::*;
5379        if op == Neg {
5380            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5381            return Ok(self.new_bigint(-x));
5382        }
5383        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5384            (Some(x), Some(y)) => (x, y),
5385            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5386            _ => {
5387                return Err(type_error(
5388                    "Cannot mix BigInt and other types, use explicit conversions",
5389                ))
5390            }
5391        };
5392        let r = match op {
5393            Add => x + y,
5394            Sub => x - y,
5395            Mul => x * y,
5396            Div => {
5397                if y.is_zero() {
5398                    return Err("RangeError: Division by zero".into());
5399                }
5400                x / y // truncates toward zero (matches JS BigInt division)
5401            }
5402            Mod => {
5403                if y.is_zero() {
5404                    return Err("RangeError: Division by zero".into());
5405                }
5406                x % y // sign follows the dividend (truncated), like JS
5407            }
5408            Pow => {
5409                if y.is_negative() {
5410                    return Err("RangeError: Exponent must be positive".into());
5411                }
5412                let exp = num_traits::ToPrimitive::to_u32(&y)
5413                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5414                num_traits::Pow::pow(x, exp)
5415            }
5416            _ => return Err(type_error("unsupported BigInt operation")),
5417        };
5418        Ok(self.new_bigint(r))
5419    }
5420
5421    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5422    /// be BigInt (mixing throws).
5423    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5424        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5425            (Some(x), Some(y)) => (x, y),
5426            _ => {
5427                return Err(type_error(
5428                    "Cannot mix BigInt and other types, use explicit conversions",
5429                ))
5430            }
5431        };
5432        let r = match tag {
5433            binop::BITAND => x & y,
5434            binop::BITOR => x | y,
5435            binop::BITXOR => x ^ y,
5436            binop::SHL => {
5437                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5438                if n >= 0 {
5439                    x << (n as usize)
5440                } else {
5441                    x >> ((-n) as usize)
5442                }
5443            }
5444            binop::SHR => {
5445                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5446                if n >= 0 {
5447                    x >> (n as usize)
5448                } else {
5449                    x << ((-n) as usize)
5450                }
5451            }
5452            binop::USHR => {
5453                return Err(type_error(
5454                    "BigInts have no unsigned right shift, use >> instead",
5455                ))
5456            }
5457            _ => return Err(type_error("unsupported BigInt operation")),
5458        };
5459        Ok(self.new_bigint(r))
5460    }
5461
5462    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5463    /// being BigInt was already handled by `strict_eq`.
5464    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5465        // Order so `big` is the BigInt side and `other` the counterpart.
5466        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5467            (Some(x), _) => (x, b),
5468            (_, Some(y)) => (y, a),
5469            _ => return false,
5470        };
5471        match other {
5472            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5473            Value::Int(n) => big == num_bigint::BigInt::from(*n),
5474            Value::Float(f) => {
5475                // Equal only when the float is an integer with the same value.
5476                if !f.is_finite() || f.fract() != 0.0 {
5477                    return false;
5478                }
5479                bigint_to_f64(&big) == *f
5480            }
5481            Value::Str(s) => match parse_bigint_str(s) {
5482                Some(bs) => big == bs,
5483                None => false,
5484            },
5485            Value::Obj(_) => match self.get(other) {
5486                // A heap string parses like a primitive string.
5487                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5488                _ => {
5489                    // Other objects reduce via ToPrimitive (their string form).
5490                    let s = self.str_of(other);
5491                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5492                }
5493            },
5494            _ => false,
5495        }
5496    }
5497}
5498
5499/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5500/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5501pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5502    let t = crate::utf16::js_trim(s);
5503    if t.is_empty() {
5504        return Some(num_bigint::BigInt::from(0));
5505    }
5506    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5507        (16, h)
5508    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5509        (8, o)
5510    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5511        (2, bb)
5512    } else {
5513        (10, t)
5514    };
5515    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5516}
5517
5518/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5519/// out-of-range magnitudes become ±Infinity, matching Node.
5520pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5521    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5522        if num_traits::Signed::is_negative(b) {
5523            f64::NEG_INFINITY
5524        } else {
5525            f64::INFINITY
5526        }
5527    })
5528}
5529
5530/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5531fn js_mod(a: f64, b: f64) -> f64 {
5532    a % b
5533}
5534
5535/// Cycle bookkeeping for one `util.inspect` render.
5536///
5537/// `seen` is the chain of objects currently being rendered (an entry appearing
5538/// twice is a back-edge), and `refs` records every object a back-edge pointed
5539/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5540/// in `[Circular *N]` / `<ref *N>`.
5541/// How an array-shaped group is laid out, beyond its entries themselves.
5542#[derive(Clone, Copy)]
5543struct ArrayLayout<'a> {
5544    /// Extra own properties follow the elements, which suppresses grid grouping.
5545    has_props: bool,
5546    /// `output`'s last entry is the `... N more items` tail rather than a real
5547    /// element, so the grid must not size a column to it.
5548    has_tail: bool,
5549    /// A constructor tag printed before the brackets, with a trailing space
5550    /// (`"Uint8Array(3) "`), or empty for a plain array.
5551    base: &'a str,
5552}
5553
5554#[derive(Default)]
5555struct InspectCycles {
5556    seen: Vec<Value>,
5557    refs: Vec<Value>,
5558    /// The indent level of the value most recently EXPANDED — node's
5559    /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5560    /// while `currentDepth - thisDepth < compact`, so without it a deeply
5561    /// nested object printed on one line where node breaks the outer levels.
5562    deepest: usize,
5563}
5564
5565impl InspectCycles {
5566    /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5567    fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5568        if let Some(id) = self.id_of(h, v) {
5569            return id;
5570        }
5571        self.refs.push(v.clone());
5572        self.refs.len()
5573    }
5574
5575    /// The `*N` id already assigned to `v`, if any.
5576    fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5577        self.refs
5578            .iter()
5579            .position(|p| h.strict_eq(p, v))
5580            .map(|i| i + 1)
5581    }
5582}
5583
5584thread_local! {
5585    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5586    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5587    /// overrides it for one call, `console.log`/`util.format` use the default.
5588    /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5589    /// "already past the limit" — everything collapses to `[Object]` at the top
5590    /// level. Held as `usize` it read as an enormous depth and expanded fully.
5591    static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5592
5593    /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5594    /// group is put on one line only when the subtree below it is shallower
5595    /// than this. `compact: false` is held as 0, which no subtree depth is
5596    /// below, so every group breaks — which is exactly what node does.
5597    static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5598
5599    /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5600    /// itself passes 80.
5601    static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5602
5603    /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5604    /// order instead of insertion order. Off by default. `assert`'s diff renderer
5605    /// turns it on so that two objects built with the same keys in a different
5606    /// order diff as equal rather than as a wholesale rewrite.
5607    static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5608
5609    /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5610    /// the rest collapse into `... N more items`. Node's default is 100;
5611    /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5612    static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5613
5614    /// `util.inspect`'s `customInspect` option: whether a value's own
5615    /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5616    /// it off so a diff shows an object's real structure rather than whatever
5617    /// summary it prefers to print.
5618    static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5619
5620    /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5621    /// carries — an array's `length`, a typed array's element width and window
5622    /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5623    static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5624}
5625
5626/// Set the `util.inspect` `showHidden` option for the next render.
5627pub fn set_inspect_show_hidden(s: bool) {
5628    INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5629}
5630
5631pub(crate) fn inspect_show_hidden() -> bool {
5632    INSPECT_SHOW_HIDDEN.with(|x| x.get())
5633}
5634
5635/// Set the `util.inspect` `customInspect` option for the next render.
5636pub fn set_inspect_custom(c: bool) {
5637    INSPECT_CUSTOM.with(|x| x.set(c));
5638}
5639
5640pub(crate) fn inspect_custom() -> bool {
5641    INSPECT_CUSTOM.with(|x| x.get())
5642}
5643
5644/// Set the `util.inspect` `sorted` option for the next render.
5645pub fn set_inspect_sorted(s: bool) {
5646    INSPECT_SORTED.with(|x| x.set(s));
5647}
5648
5649pub(crate) fn inspect_sorted() -> bool {
5650    INSPECT_SORTED.with(|x| x.get())
5651}
5652
5653/// Set the `util.inspect` `maxArrayLength` for the next render.
5654pub fn set_inspect_max_array_length(n: usize) {
5655    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5656}
5657
5658pub(crate) fn inspect_max_array_length() -> usize {
5659    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5660}
5661
5662/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5663pub fn set_inspect_compact(c: i64) {
5664    INSPECT_COMPACT.with(|x| x.set(c));
5665}
5666
5667/// Set the `util.inspect` `breakLength` for the next render.
5668pub fn set_inspect_break_length(n: usize) {
5669    INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5670}
5671
5672fn inspect_compact() -> i64 {
5673    INSPECT_COMPACT.with(|x| x.get())
5674}
5675
5676/// Set the `util.inspect` depth for the next render (restore to 2 after).
5677pub fn set_inspect_max_depth(d: i64) {
5678    INSPECT_MAX_DEPTH.with(|c| c.set(d));
5679}
5680/// Twice the configured depth, which is what the inspect walk compares its
5681/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5682/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5683/// overflowed and panicked the process — an abort no script could catch.
5684fn inspect_indent_limit() -> i64 {
5685    inspect_max_depth().saturating_mul(2)
5686}
5687
5688fn inspect_max_depth() -> i64 {
5689    INSPECT_MAX_DEPTH.with(|c| c.get())
5690}
5691
5692/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5693/// reinterpret as signed.
5694///
5695/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5696/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5697/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5698/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5699/// power-of-two modulus is exact for every finite double, so this is the whole
5700/// fix — and it is the form `Math.clz32` already used.
5701pub(crate) fn to_int32(f: f64) -> i32 {
5702    to_uint32(f) as i32
5703}
5704pub(crate) fn to_uint32(f: f64) -> u32 {
5705    if !f.is_finite() {
5706        return 0;
5707    }
5708    f.trunc().rem_euclid(4294967296.0) as u32
5709}
5710
5711/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5712fn str_to_number(s: &str) -> f64 {
5713    let t = crate::utf16::js_trim(s);
5714    if t.is_empty() {
5715        return 0.0;
5716    }
5717    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5718        return i64::from_str_radix(hex, 16)
5719            .map(|n| n as f64)
5720            .unwrap_or(f64::NAN);
5721    }
5722    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5723        return i64::from_str_radix(oct, 8)
5724            .map(|n| n as f64)
5725            .unwrap_or(f64::NAN);
5726    }
5727    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5728        return i64::from_str_radix(bin, 2)
5729            .map(|n| n as f64)
5730            .unwrap_or(f64::NAN);
5731    }
5732    match t {
5733        "Infinity" | "+Infinity" => f64::INFINITY,
5734        "-Infinity" => f64::NEG_INFINITY,
5735        _ => t.parse::<f64>().unwrap_or(f64::NAN),
5736    }
5737}
5738
5739/// `util.inspect` break length (the width past which entries wrap). Node's default.
5740fn break_length() -> usize {
5741    INSPECT_BREAK_LENGTH.with(|x| x.get())
5742}
5743/// Node's default `compact` setting (the `compact * 4` column cap term).
5744/// Node's DEFAULT `compact` setting, and the initial value of
5745/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5746/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5747/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5748const DEFAULT_COMPACT: i64 = 3;
5749/// Node's default `maxArrayLength` — the initial value of
5750/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5751/// overrides per call. Read it through `inspect_max_array_length()`, never
5752/// directly: as a bare constant the option had no effect and a 120-element array
5753/// was truncated at 100 even under `maxArrayLength: Infinity`.
5754pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5755
5756/// Whether `output` fits on a single line — a faithful port of Node's
5757/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5758/// length (braces + indentation + slack).
5759fn is_below_break_length(output: &[String], start: usize) -> bool {
5760    let limit = break_length();
5761    let mut total = output.len() + start;
5762    if total + output.len() > limit {
5763        return false;
5764    }
5765    for o in output {
5766        if o.contains('\n') {
5767            return false;
5768        }
5769        total += o.chars().count();
5770        if total > limit {
5771            return false;
5772        }
5773    }
5774    true
5775}
5776
5777/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5778/// already-formatted element strings into an aligned multi-column grid. Returns
5779/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5780/// ungrouped (so the caller falls back to single-line / one-per-line).
5781fn group_array_elements(
5782    host: &JsHost,
5783    output: &[String],
5784    values: &[Value],
5785    indentation_lvl: usize,
5786    has_tail: bool,
5787) -> (Vec<String>, bool) {
5788    let separator_space = 2usize; // ", " between entries
5789                                  // A `... N more items` tail is not an element: node drops it from the grid
5790                                  // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5791                                  // re-appends it as its own final line.
5792    let output_length = output.len() - usize::from(has_tail);
5793    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5794    let mut total_length = 0usize;
5795    let mut max_length = 0usize;
5796    for &len in &data_len[..output_length] {
5797        total_length += len + separator_space;
5798        if len > max_length {
5799            max_length = len;
5800        }
5801    }
5802    let actual_max = max_length + separator_space;
5803    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5804    if !(actual_max * 3 + indentation_lvl < break_length()
5805        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5806    {
5807        return (output.to_vec(), false);
5808    }
5809    let approx_char_heights = 2.5f64;
5810    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5811    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5812    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5813    let columns = [
5814        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5815            as i64,
5816        ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5817        inspect_compact().saturating_mul(4),
5818        15,
5819    ]
5820    .into_iter()
5821    .min()
5822    .unwrap();
5823    if columns <= 1 {
5824        return (output.to_vec(), false);
5825    }
5826    let columns = columns as usize;
5827    // The widest entry (plus separator) in each column.
5828    let mut max_line_length = vec![0usize; columns];
5829    for (i, slot) in max_line_length.iter_mut().enumerate() {
5830        let mut line_length = 0;
5831        let mut j = i;
5832        while j < output_length {
5833            if data_len[j] > line_length {
5834                line_length = data_len[j];
5835            }
5836            j += columns;
5837        }
5838        *slot = line_length + separator_space;
5839    }
5840    // Right-align (padStart) only when every element is a number/bigint.
5841    let pad_start = values.iter().all(|v| {
5842        matches!(v, Value::Int(_) | Value::Float(_))
5843            || matches!(host.get(v), Some(JsObj::BigInt(_)))
5844    });
5845    let mut tmp = Vec::new();
5846    let mut i = 0;
5847    while i < output_length {
5848        let max = (i + columns).min(output_length);
5849        let mut str_line = String::new();
5850        let mut j = i;
5851        while j < max.saturating_sub(1) {
5852            // `output[j]` has no colors here, so padding == max_line_length[col].
5853            let col = j - i;
5854            let cell = format!("{}, ", output[j]);
5855            let target = max_line_length[col];
5856            str_line.push_str(&pad_to(&cell, target, pad_start));
5857            j += 1;
5858        }
5859        // The last cell of the row: right-aligned entries pad without the ", ".
5860        if pad_start {
5861            let col = j - i;
5862            let target = max_line_length[col] - separator_space;
5863            str_line.push_str(&pad_to(&output[j], target, true));
5864        } else {
5865            str_line.push_str(&output[j]);
5866        }
5867        tmp.push(str_line);
5868        i += columns;
5869    }
5870    if has_tail {
5871        tmp.push(output[output_length].clone());
5872    }
5873    (tmp, true)
5874}
5875
5876/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5877/// (Padding is measured in chars; already ANSI-free here.)
5878fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5879    let len = s.chars().count();
5880    if len >= width {
5881        return s.to_string();
5882    }
5883    let fill = " ".repeat(width - len);
5884    if pad_start {
5885        format!("{fill}{s}")
5886    } else {
5887        format!("{s}{fill}")
5888    }
5889}
5890
5891/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5892/// `lib/internal/util/inspect.js`.
5893///
5894/// The quote character is chosen so the contents need as little escaping as
5895/// possible: single quotes normally, double quotes when the string contains a
5896/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5897/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5898/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5899/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5900fn quote_str(s: &str) -> String {
5901    let quote = if !s.contains('\'') {
5902        '\''
5903    } else if !s.contains('"') {
5904        '"'
5905    } else if !s.contains('`') && !s.contains("${") {
5906        '`'
5907    } else {
5908        '\''
5909    };
5910    let mut out = String::with_capacity(s.len() + 2);
5911    out.push(quote);
5912    for c in s.chars() {
5913        match c {
5914            _ if c == quote => {
5915                out.push('\\');
5916                out.push(c);
5917            }
5918            '\\' => out.push_str("\\\\"),
5919            '\u{8}' => out.push_str("\\b"),
5920            '\t' => out.push_str("\\t"),
5921            '\n' => out.push_str("\\n"),
5922            '\u{c}' => out.push_str("\\f"),
5923            '\r' => out.push_str("\\r"),
5924            '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5925            _ => out.push(c),
5926        }
5927    }
5928    out.push(quote);
5929    out
5930}
5931
5932/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5933fn fmt_key(k: &str) -> String {
5934    let ok = !k.is_empty()
5935        && k.chars()
5936            .next()
5937            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5938            .unwrap_or(false)
5939        && k.chars()
5940            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5941    if ok {
5942        k.to_string()
5943    } else {
5944        quote_str(k)
5945    }
5946}
5947
5948// ── iteration ────────────────────────────────────────────────────────────────
5949
5950impl JsHost {
5951    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5952    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5953    /// holds no host borrow across resumes.
5954    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5955        match self.get(v) {
5956            Some(JsObj::Array(items)) => Ok(items.clone()),
5957            Some(JsObj::Str(s)) => {
5958                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5959                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5960            }
5961            // A live array iterator, read from where its cursor stands. The
5962            // backing slots only: `iter_all` steps one through `[[Get]]` instead.
5963            Some(JsObj::Iter {
5964                idx,
5965                array: Some((arr, kind)),
5966                ..
5967            }) => {
5968                let (idx, kind) = (*idx, *kind);
5969                let items = match self.get(arr) {
5970                    Some(JsObj::Array(items)) if idx < items.len() => items[idx..].to_vec(),
5971                    _ => Vec::new(),
5972                };
5973                Ok(items
5974                    .into_iter()
5975                    .enumerate()
5976                    .map(|(n, v)| {
5977                        let key = Value::Float((idx + n) as f64);
5978                        match kind {
5979                            ArrayIterKind::Keys => key,
5980                            ArrayIterKind::Values => v,
5981                            ArrayIterKind::Entries => self.new_array(vec![key, v]),
5982                        }
5983                    })
5984                    .collect())
5985            }
5986            Some(JsObj::Iter { items, idx, .. }) => Ok(items[*idx..].to_vec()),
5987            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5988            Some(JsObj::Map { entries, .. }) => {
5989                // Map iterates as `[key, value]` pairs.
5990                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5991                Ok(pairs
5992                    .into_iter()
5993                    .map(|(k, v)| self.new_array(vec![k, v]))
5994                    .collect())
5995            }
5996            // A `Buffer` iterates over its BYTES and a typed array over its
5997            // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5998            // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5999            // "object is not iterable", which is the same invariant holding at
6000            // one of its two sites.
6001            Some(JsObj::Object(props))
6002                if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
6003            {
6004                // Iterating a view over a DETACHED buffer throws, naming the
6005                // `values` iterator — reading its elements answers zero length,
6006                // but spreading it is a method call and does not.
6007                if crate::stdlib::typedarray::view_detached_h(self, v) {
6008                    return Err(crate::stdlib::typedarray::detached_error(
6009                        "%TypedArray%.prototype",
6010                        "values",
6011                        false,
6012                    ));
6013                }
6014                Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
6015            }
6016            // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
6017            // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
6018            // produced `number is not iterable`, which no engine emits.
6019            _ => {
6020                let shown = self.inspect(v);
6021                Err(type_error(&format!("{shown} is not iterable")))
6022            }
6023        }
6024    }
6025
6026    /// Enumerable string keys of an object/array (for `for-in`). Internal
6027    /// symbol-keyed props (`@@…`) are not enumerable.
6028    /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
6029    /// not already seen, walking the whole prototype chain. Class methods and the
6030    /// builtin prototypes are non-enumerable, so in practice this only surfaces
6031    /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
6032    /// is exactly the constructor-function idiom older packages are written in.
6033    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
6034        let mut keys = self.own_enum_key_names(v);
6035        let mut cur = self.proto_of(v);
6036        let mut hops = 0;
6037        while let Some(p) = cur {
6038            // A cyclic or pathologically deep chain must not hang the loop.
6039            hops += 1;
6040            if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
6041                break;
6042            }
6043            for k in self.own_enum_key_names(&p) {
6044                if !keys.contains(&k) {
6045                    keys.push(k);
6046                }
6047            }
6048            cur = self.proto_of(&p);
6049        }
6050        keys.into_iter().map(|k| self.new_str(k)).collect()
6051    }
6052
6053    /// The own *enumerable* string keys of `v`, in property order — the single
6054    /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6055    /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6056    /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6057    /// `prop_attrs` are excluded.
6058    pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6059        self.own_key_names(v, true)
6060    }
6061
6062    /// Own string keys of `v` in insertion order. `enum_only` drops the
6063    /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6064    /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6065    pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6066        let mut keys = self.own_enum_data_keys(v, enum_only);
6067        // A global a SCRIPT created (`x = 1` with no declaration) is an own
6068        // ENUMERABLE property of the global object, but lives in the globals map
6069        // rather than in its property map — so no listing saw it, while
6070        // `globalThis.x` read it back and its descriptor called it enumerable.
6071        if self.is_global_object(v) {
6072            for k in self.globals.keys() {
6073                if !keys.contains(k) {
6074                    keys.push(k.clone());
6075                }
6076            }
6077        }
6078        // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6079        // `RegExpObj` struct, not a property map — so nothing above can list it.
6080        // Non-enumerable, so only `getOwnPropertyNames` sees it.
6081        if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6082            keys.push("lastIndex".to_string());
6083        }
6084        // An accessor defined before its object had any ordering marker (a class
6085        // prototype accessor, say) still has to appear.
6086        for k in self.own_accessor_keys(v) {
6087            if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6088                keys.push(k);
6089            }
6090        }
6091        keys
6092    }
6093
6094    /// The keys that own a slot in the object's property map, in insertion
6095    /// order, resolving accessor ordering markers back to their real key.
6096    /// Every global a SCRIPT created, in creation order — the own enumerable
6097    /// keys of the global object that live in the globals map rather than in
6098    /// its property map. `x = 1` with no declaration makes one, and
6099    /// `Object.keys(globalThis)` reports it in node.
6100    pub fn script_global_names(&self) -> Vec<String> {
6101        self.globals.keys().cloned().collect()
6102    }
6103    /// Drop a global a script created. Reports whether it was there.
6104    pub fn remove_global(&mut self, name: &str) -> bool {
6105        self.globals.shift_remove(name).is_some()
6106    }
6107    fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6108        match self.get(v) {
6109            // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6110            // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6111            // the `length`/`byteLength` view metadata, which V8 keeps on the
6112            // prototype chain or as non-enumerable own slots.
6113            // A `Buffer` and every other typed array are index-keyed exotics:
6114            // their own enumerable keys are `"0".."len-1"` (the elements live in
6115            // a hidden slot), never the `length`/`byteLength` view metadata,
6116            // which V8 keeps on the prototype chain or as non-enumerable own
6117            // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6118            // and `JSON.stringify(u8)` was `{}` where node gives
6119            // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6120            // the two views of the same question disagreed.
6121            Some(JsObj::Object(props))
6122                if matches!(
6123                    props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6124                    Some("Buffer") | Some("TypedArray")
6125                ) =>
6126            {
6127                // A view over a DETACHED buffer has no index properties at all:
6128                // its own `length` still holds the old count, so reading that
6129                // back left `Object.keys` listing eight names over no bytes.
6130                if crate::stdlib::typedarray::view_detached_h(self, v) {
6131                    return Vec::new();
6132                }
6133                // A Buffer counts its byte store; every other view reports the
6134                // element count of its window onto the ArrayBuffer.
6135                let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6136                    Some(JsObj::Array(items)) => items.len(),
6137                    _ => props
6138                        .get("length")
6139                        .map(|l| self.to_number(l))
6140                        .unwrap_or(0.0) as usize,
6141                };
6142                (0..n).map(|i| i.to_string()).collect()
6143            }
6144            Some(JsObj::Object(props)) => props
6145                .keys()
6146                .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6147                    Some(real) => Some(real.to_string()),
6148                    None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6149                    None => None,
6150                })
6151                .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6152                .collect(),
6153            // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6154            // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6155            // Without this arm every whole-object view of a string primitive was
6156            // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6157            // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6158            // `'ab'.length` answered normally, so the two views disagreed. The
6159            // spread form `{...'ab'}` went through a different path and was
6160            // already right, which is what made the gap easy to miss.
6161            Some(JsObj::Str(s)) => {
6162                let mut keys: Vec<String> =
6163                    (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6164                if !enum_only {
6165                    keys.push("length".into());
6166                }
6167                keys
6168            }
6169            // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6170            // ascending, then the exotic non-enumerable `length`, then the
6171            // ordinary string keys in insertion order. Those ordinary keys have
6172            // no property map to live in — a `str.match()` result's
6173            // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6174            // in the fn-prop side table — so they are read back from there.
6175            Some(JsObj::Array(items)) => {
6176                // An ELIDED element is not an own property at all, so it
6177                // contributes no key — the difference behind
6178                // `Object.keys([1,,3])` being `['0','2']`.
6179                let mut keys: Vec<String> = (0..items.len())
6180                    .filter(|i| !self.is_hole(v, *i))
6181                    .map(|i| i.to_string())
6182                    .collect();
6183                if !enum_only {
6184                    keys.push("length".into());
6185                }
6186                keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6187                    !k.starts_with("@@")
6188                        && !k.starts_with('#')
6189                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6190                }));
6191                keys
6192            }
6193            // A function/class keeps every own property in the side table. Its
6194            // exotic `name`/`length`/`prototype` and its class methods are all
6195            // non-enumerable, so under `enum_only` what is left is exactly what
6196            // a script assigned; `getOwnPropertyNames` reports the exotics too,
6197            // in V8's order (`length`, `name`, `prototype`, then the rest).
6198            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6199                let mut keys: Vec<String> = Vec::new();
6200                if !enum_only {
6201                    keys.push("length".into());
6202                    keys.push("name".into());
6203                    if self.owns_prototype(v) {
6204                        keys.push("prototype".into());
6205                    }
6206                }
6207                let rest: Vec<String> = self
6208                    .fn_prop_keys(v)
6209                    .into_iter()
6210                    // An accessor's ordering marker resolves back to its real
6211                    // key, so a static getter enumerates where it was declared.
6212                    .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6213                        Some(real) => Some(real.to_string()),
6214                        None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6215                        None => None,
6216                    })
6217                    .filter(|k| {
6218                        !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6219                    })
6220                    .collect();
6221                keys.extend(rest);
6222                keys
6223            }
6224            // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6225            // members node-js implements, so a package that copies a namespace
6226            // key-by-key gets the working set instead of an empty object.
6227            Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6228            // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6229            // slots, so what a script assigned lives in the side table — and is
6230            // just as much an own property as an object's.
6231            Some(_) => self
6232                .fn_prop_keys(v)
6233                .into_iter()
6234                .filter(|k| {
6235                    !k.starts_with("@@")
6236                        && !k.starts_with('#')
6237                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6238                })
6239                .collect(),
6240            _ => Vec::new(),
6241        }
6242    }
6243
6244    /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6245    /// through the byte store; everything else reads the property map. Own
6246    /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6247    /// their getters, which cannot happen under the host borrow.
6248    pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6249        self.own_enum_key_names(v)
6250            .into_iter()
6251            .map(|k| {
6252                let val = match self.get(v) {
6253                    // A Buffer's index keys read out of the hidden `@@bytes`
6254                    // array; resolve inline rather than through
6255                    // `buffer::byte_get`, which would re-borrow the host.
6256                    Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6257                        // A Buffer's elements live in `@@bytes` and every
6258                        // other typed array's in `@@elems`; both are index
6259                        // keys with no entry in the property map.
6260                        match k.parse::<usize>() {
6261                            Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6262                                .get(i)
6263                                .cloned()
6264                                .unwrap_or(Value::Undef),
6265                            _ => Value::Undef,
6266                        }
6267                    }),
6268                    // A Map/Set/Promise/RegExp/generator keeps every own
6269                    // property in the side table.
6270                    Some(
6271                        JsObj::Map { .. }
6272                        | JsObj::Set { .. }
6273                        | JsObj::Promise { .. }
6274                        | JsObj::RegExp(_)
6275                        | JsObj::Generator { .. }
6276                        | JsObj::Symbol { .. }
6277                        | JsObj::BigInt(_)
6278                        | JsObj::Iter { .. },
6279                    ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6280                    // An index reads the element; any other own key (`foo`,
6281                    // a match result's `index`) lives in the side table.
6282                    Some(JsObj::Array(items)) => k
6283                        .parse::<usize>()
6284                        .ok()
6285                        .and_then(|i| items.get(i).cloned())
6286                        .or_else(|| self.fn_prop(v, &k))
6287                        .unwrap_or(Value::Undef),
6288                    Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6289                        self.fn_prop(v, &k).unwrap_or(Value::Undef)
6290                    }
6291                    _ => Value::Undef,
6292                };
6293                (k, val)
6294            })
6295            .collect()
6296    }
6297}
6298
6299/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6300/// accessor's getter invoked — the observable shape `Object.values`,
6301/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6302/// outside a `with_host` borrow because a getter re-enters the host.
6303pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6304    // A Proxy has no property map at all: its own enumerable entries come from
6305    // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6306    // throws surfaces as an empty result here because this signature is
6307    // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6308    // and friends) go through `builtins::object_keys`, which does.
6309    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6310        return crate::proxy::own_enum_entries(v);
6311    }
6312    // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6313    // all: its members are resolved on demand by `namespace_property`, which
6314    // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6315    // Without this, spread and `Object.assign` copied the namespace's KEYS with
6316    // `undefined` for every value — measured against node v26.7.0,
6317    // `{...require('path')}.join` was `undefined` here and a function there,
6318    // while `Object.entries(require('path'))` (which resolves through
6319    // `builtins`, not through this borrow) was already correct. Two enumeration
6320    // paths, one of them silently value-less.
6321    if let Some(ns) = with_host(|h| match h.get(v) {
6322        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6323        _ => None,
6324    }) {
6325        return Ok(with_host(|h| h.own_enum_key_names(v))
6326            .into_iter()
6327            .map(|k| {
6328                let val = crate::builtins::namespace_property(&ns, &k);
6329                (k, val)
6330            })
6331            .collect());
6332    }
6333    // A string primitive's own entries are its code units. `own_enum_entries`
6334    // cannot build them: allocating the one-character string for each index
6335    // needs `&mut` host access, and it runs under a shared borrow.
6336    if let Some(sv) = with_host(|h| match h.get(v) {
6337        Some(JsObj::Str(s)) => Some(s.clone()),
6338        _ => None,
6339    }) {
6340        let units = crate::utf16::Units::of(&sv);
6341        return Ok(with_host(|h| {
6342            (0..units.len())
6343                .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6344                .collect()
6345        }));
6346    }
6347    let accessor_keys: Vec<String> = with_host(|h| {
6348        h.own_accessor_keys(v)
6349            .into_iter()
6350            .filter(|k| h.prop_attrs(v, k).enumerable)
6351            .collect()
6352    });
6353    let entries = with_host(|h| h.own_enum_entries(v));
6354    // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6355    // object spread and `JSON.stringify` all read through here, and every one
6356    // of them swallowed the exception and reported the property as absent (or
6357    // as `null`) instead.
6358    let mut out = Vec::with_capacity(entries.len());
6359    for (k, val) in entries {
6360        if accessor_keys.contains(&k) {
6361            out.push((k.clone(), get_prop_chain(v, &k)?));
6362        } else {
6363            out.push((k, val));
6364        }
6365    }
6366    Ok(out)
6367}
6368
6369// ── function invocation ──────────────────────────────────────────────────────
6370
6371/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6372/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6373/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6374/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6375/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6376/// Float→i64/f64 per the export signature).
6377fn marshal_ffi_arg(v: &Value) -> Value {
6378    match v {
6379        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6380            Some(s) => Value::str(s),
6381            None => v.clone(),
6382        },
6383        _ => v.clone(),
6384    }
6385}
6386
6387/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6388pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6389    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6390    // line)`; compile + register the block's exported functions, returning JS
6391    // `undefined` (`Value::Undef`).
6392    if name == "__rust_compile" {
6393        let b64 = args
6394            .first()
6395            .map(|v| with_host(|h| h.str_of(v)))
6396            .unwrap_or_default();
6397        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6398    }
6399    if let Some(v) = with_host(|h| h.read_name(name)) {
6400        return invoke(&v, args, None);
6401    }
6402    // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6403    // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6404    // and `[eval][0](src)` all reach the same function value but are INDIRECT
6405    // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6406    // This is the one place the two forms are distinguishable without a compiler
6407    // change: `call_named` is reached only from `ops::CALL`, which the compiler
6408    // emits exclusively for a bare-identifier callee, while every value-call form
6409    // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6410    // has already established that `eval` is not shadowed by a user binding.
6411    if name == "eval" {
6412        return crate::builtins::eval_source(args.first(), true);
6413    }
6414    if crate::builtins::is_known_builtin(name) {
6415        return crate::builtins::call_builtin_function(name, args);
6416    }
6417    // A `rust { ... }` block's exported functions are callable by bareword.
6418    // Reached only after user names/globals and builtins all miss, so JS code
6419    // always wins; the registry membership check keeps this off the hot path.
6420    if fusevm::ffi::is_registered(name) {
6421        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6422        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6423            return r;
6424        }
6425    }
6426    Err(ref_error(name))
6427}
6428
6429thread_local! {
6430    /// The constructor a builtin STATIC is currently being invoked on.
6431    ///
6432    /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6433    /// builtin, which is reached by NAME and so cannot see `A`. The species
6434    /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6435    /// their result with `this`, so on a subclass they must construct through
6436    /// it. A stack, since one static can call another.
6437    static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6438        const { std::cell::RefCell::new(Vec::new()) };
6439}
6440
6441/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6442pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6443    STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6444    let out = f();
6445    STATIC_THIS.with(|s| {
6446        s.borrow_mut().pop();
6447    });
6448    out
6449}
6450
6451/// The constructor the running builtin static was called on, if it was reached
6452/// through a subclass rather than directly.
6453pub fn current_static_this() -> Option<Value> {
6454    STATIC_THIS.with(|s| s.borrow().last().cloned())
6455}
6456
6457/// `recv.name(args)`.
6458pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6459    // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6460    // the failure is the property read, not the call: node reports
6461    // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6462    // whole method dispatch against the nullish receiver, found nothing, and
6463    // reported `undefined.foo is not a function` — the wrong error class of
6464    // message for the single most common runtime fault in JS, and one that
6465    // points at the callee instead of at the base that was nullish.
6466    if with_host(|h| h.is_nullish(recv)) {
6467        return Err(type_error(&format!(
6468            "Cannot read properties of {} (reading '{name}')",
6469            with_host(|h| h.str_of(recv))
6470        )));
6471    }
6472    // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6473    // comes first: an unbranded receiver throws here rather than reporting the
6474    // method missing. Only a `#`-prefixed name pays the extra probe.
6475    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6476        return Err(crate::builtins::private_brand_message(name, false));
6477    }
6478    // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6479    // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6480    // below all read a property map a proxy does not have.
6481    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6482        let f = crate::builtins::get_property(recv, name)?;
6483        if !with_host(|h| is_callable(h, &f)) {
6484            return Err(type_error(&format!("{name} is not a function")));
6485        }
6486        // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6487        // `Object.prototype` methods are generic over `this`. node-js models each
6488        // as a thunk BOUND to the object it was read off — through a proxy, that
6489        // is the target — so invoking the thunk answers for the target and skips
6490        // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6491        // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6492        // those against the PROXY, which is the `this` the real method receives.
6493        //
6494        // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6495        // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6496        // an array stringifies `1,2` through `Array.prototype.toString`, not
6497        // `[object Object]`), which the bound thunk already gets right.
6498        if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6499            if with_host(|h| is_callable(h, recv)) {
6500                if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6501                    return Ok(r);
6502                }
6503            }
6504            if matches!(
6505                name,
6506                "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6507            ) {
6508                return crate::builtins::object_builtin_method(recv, name, args);
6509            }
6510            // The three above resolve by the TARGET's kind, and the thunk is
6511            // already bound to the target — so it must be invoked WITHOUT a
6512            // receiver override. Passing the proxy as `this` made the
6513            // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6514            // call straight back into this branch, so `String(new Proxy({}, {}))`
6515            // recursed until the stack overflowed and the process aborted.
6516            if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6517                return invoke(&f, args, None);
6518            }
6519        }
6520        return invoke(&f, args, Some(recv.clone()));
6521    }
6522    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6523    // name.
6524    if let Some(ns) = with_host(|h| match h.get(recv) {
6525        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6526        _ => None,
6527    }) {
6528        let qualified = format!("{ns}.{name}");
6529        if crate::builtins::is_known_builtin(&qualified) {
6530            return crate::builtins::call_builtin_function(&qualified, args);
6531        }
6532    }
6533    // Object / instance: an accessor getter that yields a function, an own or
6534    // inherited method (class methods live on the prototype chain), then an
6535    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6536    // directly — NOT get_property — so the Object.prototype-builtin fallback
6537    // never routes back through a BoundMethod and recurses.
6538    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6539        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6540        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6541        // A user-added or reparented-prototype method takes precedence over the
6542        // native dispatcher — matching JS resolution order (own → prototype
6543        // chain). This is what lets Express work: it does
6544        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6545        // where `send` is a plain function on the reparented prototype. Native
6546        // instance methods (`res.end`/`write`/…) are NOT stored as plain
6547        // function properties, so `lookup_chain` misses them and we fall through
6548        // to `instance_call` for the real native behavior.
6549        if let Some(tag) = crate::stdlib::native_tag(recv) {
6550            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6551                if with_host(|h| is_callable(h, &f)) {
6552                    return invoke(&f, args, Some(recv.clone()));
6553                }
6554            }
6555            // `Object.prototype` methods reach a native instance too — a Buffer
6556            // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6557            // chain, and the native dispatcher has no entry for them.
6558            if crate::builtins::is_object_builtin_method(name)
6559                && !crate::stdlib::instance_has_method(&tag, name)
6560            {
6561                return crate::builtins::object_builtin_method(recv, name, args);
6562            }
6563            return crate::stdlib::instance_call(&tag, recv, name, args);
6564        }
6565        // A primitive wrapper forwards to the primitive's method table, the
6566        // same way a native instance forwards to its tag's. A user method on
6567        // the wrapper or anywhere on its chain still wins first.
6568        if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6569            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6570                if with_host(|h| is_callable(h, &f)) {
6571                    return invoke(&f, args, Some(recv.clone()));
6572                }
6573            }
6574            // The reflective `Object.prototype` methods answer for the WRAPPER
6575            // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6576            // properties, not about the string.
6577            if crate::builtins::is_object_builtin_method(name) {
6578                return crate::builtins::object_builtin_method(recv, name, args);
6579            }
6580            return call_method(&prim, name, args);
6581        }
6582        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6583            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6584            if with_host(|h| is_callable(h, &f)) {
6585                return invoke(&f, args, Some(recv.clone()));
6586            }
6587        }
6588        // A Proxy in the prototype chain serves the method through its `get`
6589        // trap. `lookup_chain` below reads property maps, which a proxy has none
6590        // of, so without this `child.m()` on `Object.create(proxy)` reported
6591        // "m is not a function" even though `child.m` already read correctly.
6592        if crate::builtins::proxy_proto_link(recv, name).is_some() {
6593            let f = crate::builtins::get_property(recv, name)?;
6594            if !with_host(|h| is_callable(h, &f)) {
6595                return Err(type_error(&format!("{name} is not a function")));
6596            }
6597            return invoke(&f, args, Some(recv.clone()));
6598        }
6599        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6600            if with_host(|h| is_callable(h, &f)) {
6601                return invoke(&f, args, Some(recv.clone()));
6602            }
6603            return Err(type_error(&format!("{name} is not a function")));
6604        }
6605        // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6606        // it: a plain object is not LINKED to the intrinsic prototype object,
6607        // its `Object.prototype` members are synthesized instead. So
6608        // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6609        // while `({}).tap` already read back as `f`.
6610        if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6611            if with_host(|h| is_callable(h, &f)) {
6612                return invoke(&f, args, Some(recv.clone()));
6613            }
6614        }
6615        // A method from an intrinsic prototype this object's CHAIN passes
6616        // through — `Object.create(Array.prototype).push(1)`. The read already
6617        // resolves it through the same owner oracle; dispatch reported "is not
6618        // a function", the read and the call disagreeing once more.
6619        if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6620            if owner != "Object" {
6621                return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6622            }
6623        }
6624        if crate::builtins::is_object_builtin_method(name) {
6625            return crate::builtins::object_builtin_method(recv, name, args);
6626        }
6627        if name == "constructor" {
6628            if let Some(r) = call_default_ctor(recv, &args) {
6629                return r;
6630            }
6631        }
6632        return Err(type_error(&format!("{name} is not a function")));
6633    }
6634    // Function value methods: call / apply / bind, then any static method stored
6635    // on the function object.
6636    if matches!(
6637        with_host(|h| h.kind_of(recv)),
6638        Some(ObjKind::Func)
6639            | Some(ObjKind::Class)
6640            | Some(ObjKind::BoundFunc)
6641            | Some(ObjKind::BoundMethod)
6642            | Some(ObjKind::Builtin)
6643    ) {
6644        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6645            return Ok(r);
6646        }
6647        // A static method (own or inherited): `this` is the constructor (`recv`).
6648        let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6649            with_host(|h| h.class_static(recv, name))
6650        } else {
6651            with_host(|h| h.fn_prop(recv, name))
6652        };
6653        if let Some(f) = stat {
6654            if with_host(|h| is_callable(h, &f)) {
6655                return invoke(&f, args, Some(recv.clone()));
6656            }
6657        }
6658        // `class_static` only walks user-class `extends` links, so a chain that
6659        // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6660        // could not reach that builtin's statics: `D.from([1,2])` threw
6661        // "from is not a function" even though `typeof D.from` said `function`.
6662        // Re-dispatch the call against that ancestor, which is what reaches a
6663        // builtin namespace's methods.
6664        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6665            if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6666                if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6667                    // The subclass is recorded so a species-aware static
6668                    // (`Array.from`, `Promise.resolve`, …) builds its result
6669                    // through it rather than through the builtin.
6670                    return with_static_this(recv, || call_method(&anc, name, args));
6671                }
6672            }
6673        }
6674        // A method inherited via the function's [[Prototype]] chain (set with
6675        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6676        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6677        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6678            if with_host(|h| is_callable(h, &f)) {
6679                return invoke(&f, args, Some(recv.clone()));
6680            }
6681        }
6682        // An `Object.prototype` method invoked with a builtin namespace/prototype
6683        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6684        // ownership probe) — dispatch it against the builtin receiver.
6685        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6686            && crate::builtins::is_object_builtin_method(name)
6687        {
6688            return crate::builtins::object_builtin_method(recv, name, args);
6689        }
6690    }
6691    if name == "constructor" {
6692        if let Some(r) = call_default_ctor(recv, &args) {
6693            return r;
6694        }
6695    }
6696    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6697    crate::builtins::call_type_method(recv, name, args)
6698}
6699
6700/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6701/// owns a `constructor` slot.
6702///
6703/// Reading the property already resolves a builtin instance's native constructor
6704/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6705/// consulted the prototype chain, so the two disagreed:
6706/// `(function(){}).constructor === Function` read `true` while
6707/// `(function(){}).constructor('return 9')` threw
6708/// `TypeError: constructor is not a function`. That call form is exactly how
6709/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6710/// constructor. Resolved here through the same one definition the read uses, so
6711/// the two can no longer drift apart. `None` means "not resolvable/callable",
6712/// leaving the caller's original error in place.
6713fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6714    let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6715    with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6716}
6717
6718/// Call any callable value.
6719pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6720    // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6721    // Probed by kind first so the ordinary call path never clones its arguments.
6722    if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6723        return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6724    }
6725    let obj = with_host(|h| h.get(callable).cloned());
6726    match obj {
6727        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6728        // against the invoke-time `this` (supplied by `.call`/`.apply`).
6729        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6730            let recv = this.unwrap_or(Value::Undef);
6731            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6732        }
6733        // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6734        // form a library uses to read a slot from an arbitrary receiver
6735        // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6736        // .call(m)`). It brand-checks `this` and reads, or throws naming
6737        // itself.
6738        // The setter half of the `arguments`/`caller` poison pill — the only
6739        // intrinsic accessor here that has one, and it throws like its getter.
6740        Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6741            let _ = &name;
6742            let recv = this.unwrap_or(Value::Undef);
6743            // The setter half accepts silently for the same receivers the
6744            // getter answers for, and throws for the rest.
6745            if with_host(|h| h.fn_is_sloppy(&recv)) {
6746                Ok(Value::Undef)
6747            } else {
6748                Err(type_error(crate::builtins::POISON_PILL))
6749            }
6750        }
6751        Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6752            let recv = this.unwrap_or(Value::Undef);
6753            let rest = &name["@protoget:".len()..];
6754            let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6755            crate::builtins::proto_getter_call(ctor, key, &recv)
6756        }
6757        // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6758        // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6759        // this:
6760        //
6761        //     function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6762        //     InternalDecoder.prototype = StringDecoder.prototype;
6763        //
6764        // A native constructor builds a fresh tagged object, so initializing the
6765        // SUPPLIED object means building one and moving its slots across.
6766        //
6767        // The guard is deliberately narrow: `obj` must already inherit from THIS
6768        // constructor's prototype, i.e. the subclass really did adopt it. Without
6769        // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6770        // return a string / a buffer — would start mutating `x` instead.
6771        Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6772            let target = this.expect("guard checked");
6773            let built = crate::stdlib::construct(name, &args)
6774                .expect("guard checked a native constructor")?;
6775            adopt_native_slots(&target, &built);
6776            Ok(Value::Undef)
6777        }
6778        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6779        Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6780        // A method read off an object is modelled as a thunk BOUND to it, but an
6781        // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6782        // rebinds `this`, and every `Array.prototype` method is generic over it, so
6783        // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6784        // override made that read back as the empty array the thunk was read off.
6785        // A nullish override is ignored: it carries no receiver to dispatch on.
6786        Some(JsObj::BoundMethod { recv, name }) => {
6787            let target = match &this {
6788                Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6789                _ => &recv,
6790            };
6791            // A thunk read off an ARRAY carries an `Array.prototype` method, and
6792            // those are generic over `this` — route the rebound call through
6793            // `proto_method` so an array-LIKE receiver takes the generic path
6794            // instead of being told the method does not exist.
6795            if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6796                return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6797            }
6798            call_method(target, &name, args)
6799        }
6800        Some(JsObj::BoundFunc {
6801            target,
6802            this: bthis,
6803            args: pre,
6804        }) => {
6805            let mut all = pre;
6806            all.extend(args);
6807            invoke(&target, all, Some(bthis))
6808        }
6809        Some(JsObj::Class(c)) => Err(type_error(&format!(
6810            "Class constructor {} cannot be invoked without 'new'",
6811            c.name
6812        ))),
6813        _ => Err(type_error(&format!(
6814            "{} is not a function",
6815            with_host(|h| h.str_of(callable))
6816        ))),
6817    }
6818}
6819
6820/// Whether calling the native constructor `name` with `this` is the ES5
6821/// constructor-stealing pattern rather than an ordinary call.
6822///
6823/// True only when `name` really is a native stdlib constructor AND `this` is a
6824/// plain object that already inherits from that constructor's prototype — the
6825/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6826/// object that merely happens to be passed as `this` does not qualify, so
6827/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6828fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6829    let Some(target) = this else { return false };
6830    if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6831        return false;
6832    }
6833    // Already initialized (e.g. a re-entrant call) — nothing to steal.
6834    if crate::stdlib::native_tag(target).is_some() {
6835        return false;
6836    }
6837    let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6838        return false;
6839    };
6840    let mut cur = with_host(|h| h.proto_of(target));
6841    while let Some(p) = cur {
6842        if p == proto {
6843            return true;
6844        }
6845        cur = with_host(|h| h.proto_of(&p));
6846    }
6847    false
6848}
6849
6850/// Move a freshly-constructed native instance's state onto `target`, so an
6851/// object built by a subclass constructor becomes a working instance of the
6852/// native class. Copies every own key the native constructor set — the hidden
6853/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6854/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6855fn adopt_native_slots(target: &Value, built: &Value) {
6856    let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6857        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6858        _ => Vec::new(),
6859    });
6860    with_host(|h| {
6861        if let Some(JsObj::Object(p)) = h.get_mut(target) {
6862            for (k, v) in slots {
6863                p.insert(k, v);
6864            }
6865        }
6866    });
6867}
6868
6869/// Execute a user function/closure body on a fresh frame.
6870pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6871    run_user_func_of(fv, args, this, None)
6872}
6873
6874/// [`run_user_func`] with the function VALUE the call came through, which the
6875/// `arguments` object needs for its `callee`.
6876pub fn run_user_func_of(
6877    fv: &FuncVal,
6878    args: Vec<Value>,
6879    this: Option<Value>,
6880    callee: Option<Value>,
6881) -> Result<Value, String> {
6882    run_user_func_full(fv, args, this, None, callee)
6883}
6884
6885/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6886pub fn run_user_func_nt(
6887    fv: &FuncVal,
6888    args: Vec<Value>,
6889    this: Option<Value>,
6890    new_target: Option<Value>,
6891) -> Result<Value, String> {
6892    run_user_func_full(fv, args, this, new_target, None)
6893}
6894
6895fn run_user_func_full(
6896    fv: &FuncVal,
6897    args: Vec<Value>,
6898    this: Option<Value>,
6899    new_target: Option<Value>,
6900    callee: Option<Value>,
6901) -> Result<Value, String> {
6902    // Consumed first, before anything here can start another call.
6903    let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6904    // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6905    // the entire compiled body, `sub_chunks` and all — on every single call.
6906    // The chunk is now reached once per pooled VM, in the two arms below.
6907    let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6908        let d = &h.funcs[fv.def_id];
6909        (
6910            d.params.clone(),
6911            d.is_generator,
6912            d.is_async,
6913            d.is_arrow,
6914            d.name.clone(),
6915        )
6916    });
6917    let env = new_env(fv.env.clone());
6918    // Bind the simple/rest arg slots; destructuring + defaults run in the body
6919    // prologue (compiled ahead of the user statements).
6920    let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6921    bind_params(
6922        &env,
6923        &params,
6924        args,
6925        is_arrow_def,
6926        callee.as_ref(),
6927        fn_is_sloppy && !is_arrow_def,
6928    );
6929    // Arrow functions capture `this` lexically; regular functions receive it.
6930    let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6931    // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6932    // becomes the global object. Only a strict function keeps `undefined`, and
6933    // an arrow has no `this` of its own to substitute. Leaving it undefined
6934    // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6935    // saw `undefined` where node sees `globalThis`.
6936    let sloppy_this = !fv.is_arrow
6937        && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6938        && match &this_val {
6939            None => true,
6940            Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6941        };
6942    if sloppy_this {
6943        this_val = Some(with_host(|h| h.global_object()));
6944    } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6945        // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6946        // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6947        // wrapper rather than the number. Only strict mode passes it through.
6948        if let Some(t) = this_val.clone() {
6949            let boxed = crate::builtins::to_object(&t);
6950            this_val = Some(boxed);
6951        }
6952    }
6953    // A generator function does not run its body on call — it returns a suspended
6954    // generator over the already-bound frame.
6955    if is_generator {
6956        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6957        let gen = make_generator(
6958            chunk,
6959            env,
6960            this_val,
6961            fv.home_class.clone(),
6962            fv.home_static,
6963            fv.home_object.clone(),
6964            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6965        );
6966        if is_async {
6967            if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6968                with_host(|h| h.generators[id as usize].async_gen = true);
6969            }
6970        }
6971        return Ok(gen);
6972    }
6973    // An async function runs on a coroutine and returns a Promise: it executes
6974    // synchronously up to the first `await`, then continues via microtasks.
6975    if is_async {
6976        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6977        let gen = make_generator(
6978            chunk,
6979            env,
6980            this_val,
6981            fv.home_class.clone(),
6982            fv.home_static,
6983            fv.home_object.clone(),
6984            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6985        );
6986        return Ok(run_async(gen));
6987    }
6988    let home = fv
6989        .home_class
6990        .as_ref()
6991        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6992    // Resolved BEFORE the borrow below: reading the function table re-enters
6993    // the host, and doing it inside the frame-push closure double-borrows.
6994    let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6995    with_host(|h| {
6996        h.frames.push(Frame {
6997            base_env: env.clone(),
6998            env,
6999            this_obj: this_val,
7000            new_target,
7001            home_class: home,
7002            home_static: fv.home_static,
7003            home_object: fv.home_object.clone(),
7004            strict: fn_strict,
7005            line: 0,
7006            owner: Some(def_name),
7007            is_module: false,
7008            this_state: if derived_ctor {
7009                ThisState::Pending
7010            } else {
7011                ThisState::Plain
7012            },
7013        })
7014    });
7015    let r = run_chunk_keyed(func_key(fv.def_id), || {
7016        with_host(|h| h.funcs[fv.def_id].chunk.clone())
7017    });
7018    let (sig, this_state) = with_host(|h| {
7019        let frame = h.frames.pop();
7020        (h.signal.take(), frame.map(|f| f.this_state))
7021    });
7022    let ret = match r {
7023        Err(e) => return Err(e),
7024        Ok(_) => match sig {
7025            Some(Signal::Return(v)) => v,
7026            _ => Value::Undef,
7027        },
7028    };
7029    // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
7030    // return wins; any other non-undefined return is a TypeError; and falling
7031    // off the end (or `return;`) needs `this` to have been bound by `super()`.
7032    if derived_ctor && !returns_object(&ret) {
7033        if !matches!(ret, Value::Undef) {
7034            return Err(type_error(
7035                "Derived constructors may only return object or undefined",
7036            ));
7037        }
7038        if this_state == Some(ThisState::Pending) {
7039            return Err(this_before_super_error());
7040        }
7041    }
7042    Ok(ret)
7043}
7044
7045/// Bind positional args into a fresh call environment. The compiler emits the
7046/// param names in `def.params`; a `...rest` slot collects the tail as an array.
7047fn bind_params(
7048    env: &Env,
7049    params: &[ParamSlot],
7050    args: Vec<Value>,
7051    is_arrow: bool,
7052    callee: Option<&Value>,
7053    sloppy: bool,
7054) {
7055    let mut vars = VarMap::default();
7056    let mut i = 0;
7057    for slot in params {
7058        if slot.rest {
7059            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7060            let arr = with_host(|h| h.new_array(rest));
7061            vars.insert(slot.name.clone(), arr);
7062        } else {
7063            let v = args.get(i).cloned().unwrap_or(Value::Undef);
7064            vars.insert(slot.name.clone(), v);
7065            i += 1;
7066        }
7067    }
7068    // `arguments` array (simple approximation — see BUGS.md: it is a real
7069    // Array, not an Arguments exotic). An ARROW function never gets one:
7070    // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7071    // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7072    // enclosing function's. Binding an empty one here made
7073    // `function f(){ const g = () => [...arguments]; }` see zero args.
7074    if !is_arrow {
7075        let args_arr = with_host(|h| {
7076            let a = h.new_array(args);
7077            // Marked so it can be told apart from an ordinary array: node's
7078            // `arguments` is an exotic, and without the mark
7079            // `Array.isArray(arguments)` was true, the brand was
7080            // `[object Array]` and `util.types.isArgumentsObject` was false.
7081            // The backing representation stays an Array, which is what keeps
7082            // indices, `length`, spread and `for-of` working.
7083            h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7084            // `callee` is the function itself in SLOPPY code (it is a poison
7085            // pill only in strict, which the read path handles). It read back
7086            // `undefined`, so the pre-`class` self-reference idiom
7087            // `(function(){ arguments.callee })` found nothing.
7088            if let Some(f) = callee {
7089                if sloppy {
7090                    h.set_fn_prop(&a, "@@callee", f.clone());
7091                }
7092            }
7093            a
7094        });
7095        vars.entry("arguments".to_string()).or_insert(args_arr);
7096    }
7097    env.borrow_mut().vars = vars;
7098}
7099
7100/// Construct an instance with `new` — creates a fresh object, binds it as
7101/// `this`, runs the constructor, and returns the object (unless the constructor
7102/// returns its own object).
7103pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7104    construct_nt(ctor, args, ctor.clone())
7105}
7106
7107/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7108/// calls `super(...)` — the target stays the originally-`new`ed class).
7109pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7110    // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7111    if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7112        return crate::proxy::construct(ctor, args, &new_target)
7113            .map(|r| r.expect("kind_of said Proxy"));
7114    }
7115    let obj = with_host(|h| h.get(ctor).cloned());
7116    match obj {
7117        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7118        Some(JsObj::Func(fv)) => {
7119            // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7120            // `function*` and an `async function` are callable but not
7121            // constructable (10.2.2 is installed only for the ordinary case), so
7122            // `new` on one is a TypeError — node-js instead ran the body and
7123            // handed back a half-built instance (for a generator, an object whose
7124            // constructor had returned a suspended generator).
7125            let non_ctor = with_host(|h| {
7126                h.funcs
7127                    .get(fv.def_id)
7128                    // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7129                    // is `TypeError: o.m is not a constructor` on node v26.7.0,
7130                    // which is also why a method owns no `prototype`.
7131                    .map(|d| d.is_generator || d.is_async || d.is_method)
7132                    .unwrap_or(false)
7133            });
7134            if fv.is_arrow || non_ctor {
7135                return Err(not_a_constructor(ctor));
7136            }
7137            // A plain constructor function: instance delegates to `fn.prototype`
7138            // (auto-created with a `.constructor` back-link if not yet accessed).
7139            let inst = with_host(|h| {
7140                let o = h.new_object(IndexMap::new());
7141                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7142                    let p = h.new_object(IndexMap::new());
7143                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7144                        pp.insert("constructor".to_string(), ctor.clone());
7145                    }
7146                    // `F.prototype.constructor` is non-enumerable in JS.
7147                    h.hide_prop(&p, "constructor");
7148                    h.set_fn_prop(ctor, "prototype", p.clone());
7149                    p
7150                });
7151                h.set_proto(&o, proto);
7152                o
7153            });
7154            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7155            if returns_object(&r) {
7156                Ok(r)
7157            } else {
7158                Ok(inst)
7159            }
7160        }
7161        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7162        Some(JsObj::BoundFunc {
7163            target, args: pre, ..
7164        }) => {
7165            let mut all = pre;
7166            all.extend(args);
7167            construct_nt(&target, all, new_target)
7168        }
7169        _ => Err(not_a_constructor(ctor)),
7170    }
7171}
7172
7173/// `TypeError: <callee> is not a constructor`.
7174///
7175/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7176/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7177/// its name — the same string in the common case — and anything else by its
7178/// value.
7179fn not_a_constructor(ctor: &Value) -> String {
7180    let name = with_host(|h| match h.callable_name(ctor) {
7181        n if n.is_empty() => h.str_of(ctor),
7182        n => n,
7183    });
7184    type_error(&format!("{name} is not a constructor"))
7185}
7186
7187/// Whether a constructor's return value is an object (so `new` yields it instead
7188/// of the fresh instance). In JS "object" includes functions — the `router`
7189/// package's constructor `return router` (a function) must be honored, or the
7190/// returned router loses its callable identity.
7191fn returns_object(r: &Value) -> bool {
7192    matches!(
7193        with_host(|h| h.get(r).cloned()),
7194        Some(JsObj::Object(_))
7195            | Some(JsObj::Array(_))
7196            | Some(JsObj::Map { .. })
7197            | Some(JsObj::Set { .. })
7198            | Some(JsObj::Func(_))
7199            | Some(JsObj::Class(_))
7200            | Some(JsObj::BoundFunc { .. })
7201            | Some(JsObj::BoundMethod { .. })
7202            | Some(JsObj::RegExp(_))
7203    )
7204}
7205
7206/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7207/// run field initializers + the constructor (which may call `super(...)`).
7208fn construct_class(
7209    class_val: &Value,
7210    args: Vec<Value>,
7211    new_target: Value,
7212) -> Result<Value, String> {
7213    let cv = match with_host(|h| h.get(class_val).cloned()) {
7214        Some(JsObj::Class(c)) => c,
7215        _ => return Err(type_error("not a class")),
7216    };
7217    // Resolve the prototype of the *most-derived* class being `new`ed, so an
7218    // instance created through a `super()` chain still delegates to the leaf
7219    // prototype (correct method resolution).
7220    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7221        Some(JsObj::Class(c)) => c.proto.clone(),
7222        _ => cv.proto.clone(),
7223    };
7224    let inst = with_host(|h| {
7225        let o = h.new_object(IndexMap::new());
7226        h.set_proto(&o, leaf_proto.clone());
7227        o
7228    });
7229    // A `super()` deeper in may substitute the instance; the previous value is
7230    // restored so a `new` inside a constructor body cannot be mistaken for one.
7231    let saved = with_host(|h| h.swap_super_replacement(None));
7232    let ran = run_class_ctor(&cv, &inst, args, &new_target);
7233    let substituted = with_host(|h| {
7234        let s = h.take_super_replacement();
7235        h.swap_super_replacement(saved);
7236        s
7237    });
7238    // A constructor that returns an object replaces the instance (`new`
7239    // semantics); failing that, whatever `super()` substituted for it.
7240    match ran? {
7241        Some(obj) if returns_object(&obj) => Ok(obj),
7242        _ => Ok(substituted.unwrap_or(inst)),
7243    }
7244}
7245
7246/// Run one class's field initializers then its constructor on an existing
7247/// instance. Returns the constructor's explicit object return (if any). For a
7248/// base class this is the whole init; for a derived class the constructor body
7249/// reaches `super(...)` which recurses into the parent.
7250fn run_class_ctor(
7251    cv: &ClassVal,
7252    inst: &Value,
7253    args: Vec<Value>,
7254    new_target: &Value,
7255) -> Result<Option<Value>, String> {
7256    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7257    // that. A base class initializes fields before the constructor body.
7258    if cv.parent.is_none() {
7259        init_fields(cv, inst)?;
7260    }
7261    match &cv.ctor {
7262        Some(ctor_fn) => {
7263            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7264                Some(JsObj::Func(f)) => f,
7265                _ => return Err(type_error("class constructor is not a function")),
7266            };
7267            if cv.parent.is_some() {
7268                with_host(|h| h.mark_next_call_derived_ctor());
7269            }
7270            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7271            return Ok(Some(r));
7272        }
7273        None => {
7274            // Default constructor: `constructor(...a){ super(...a); }` for a
7275            // derived class, empty for a base class.
7276            if let Some(parent) = &cv.parent {
7277                // A base constructor's returned object becomes the instance, so
7278                // the implicit `constructor(...a){ super(...a) }` hands it on.
7279                if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7280                    init_fields(cv, &replacement)?;
7281                    return Ok(Some(replacement));
7282                }
7283                init_fields(cv, inst)?;
7284            }
7285        }
7286    }
7287    Ok(None)
7288}
7289
7290/// Evaluate and assign a class's instance-field initializers on `inst`.
7291fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7292    for (name, thunk, name_anon) in &cv.fields {
7293        init_one_field(inst, name, thunk, *name_anon)?;
7294    }
7295    Ok(())
7296}
7297
7298/// Evaluate ONE instance-field initializer thunk and install the result on
7299/// `inst`.
7300///
7301/// Shared by the base-class path (`init_fields`) and the derived-class path
7302/// that runs after `super(...)`; the two used to be separate loops, and only the
7303/// first canonicalized an array-index key.
7304///
7305/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7306/// gives the function the name `f`. It is decided by the compiler from the
7307/// syntax, never from the value.
7308pub fn init_one_field(
7309    inst: &Value,
7310    name: &str,
7311    thunk: &Value,
7312    name_anon: bool,
7313) -> Result<(), String> {
7314    // The thunk is an arrow capturing the class scope; run it with `this`=inst
7315    // so `this.other`-referencing initializers work.
7316    let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7317    with_host(|h| {
7318        if name_anon {
7319            let s = h.new_str(name.to_string());
7320            h.set_fn_prop(&val, "name", s);
7321        }
7322        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7323            let is_new = !props.contains_key(name);
7324            props.insert(name.to_string(), val);
7325            if is_new && array_index(name).is_some() {
7326                canonicalize_own_keys(props);
7327            }
7328        }
7329    });
7330    Ok(())
7331}
7332
7333/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7334/// kind (class vs plain function vs builtin) using the existing instance.
7335/// Run the parent constructor against `inst`.
7336///
7337/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7338/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7339/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7340/// derived constructor writes to the object `new` will hand back.
7341pub fn super_construct(
7342    parent: &Value,
7343    args: Vec<Value>,
7344    inst: &Value,
7345    new_target: &Value,
7346) -> Result<Option<Value>, String> {
7347    match with_host(|h| h.get(parent).cloned()) {
7348        Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7349            .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7350        Some(JsObj::Func(fv)) => {
7351            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7352            Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7353        }
7354        Some(JsObj::Builtin(name)) => {
7355            let built = crate::builtins::construct_builtin(&name, args)?;
7356            // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7357            // the heap variant, not in a property map, so copying own props
7358            // cannot carry it: the instance has to BECOME the built object.
7359            // Without this `new (class extends Array {})().push` was not a
7360            // function, and the same for Map, Set, RegExp, Promise and
7361            // Function — subclassing a builtin produced a plain object.
7362            if !become_exotic(inst, &built) {
7363                // An `Error` subclass is ordinary: its state IS own properties.
7364                adopt_own_props(inst, &built);
7365            }
7366            Ok(None)
7367        }
7368        // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7369        // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7370        // to the target). node-js initializes an ALREADY-allocated `inst` rather
7371        // than adopting the constructor's return value, so what the proxy built
7372        // is moved across — the same move the builtin arm makes.
7373        Some(JsObj::Proxy { .. }) => {
7374            let built = construct_nt(parent, args, new_target.clone())?;
7375            if !become_exotic(inst, &built) {
7376                adopt_own_props(inst, &built);
7377            }
7378            Ok(None)
7379        }
7380        _ => Err(type_error("super is not a constructor")),
7381    }
7382}
7383
7384/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7385/// a parent constructor produces a fresh object but node-js's class model has
7386/// already allocated the instance `this` is bound to.
7387/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7388/// extends a builtin EXOTIC really is one.
7389///
7390/// `inst` keeps its identity and its prototype link — the leaf class's
7391/// prototype, which is what method resolution and `instanceof` walk — while its
7392/// contents become the exotic the parent constructor produced. The side tables
7393/// keyed by heap index (array holes, property attributes, the fn-prop table)
7394/// move across with it.
7395///
7396/// Returns false for a variant whose state is ordinary own properties
7397/// (`Error`), which the caller copies instead.
7398fn become_exotic(inst: &Value, built: &Value) -> bool {
7399    let exotic = matches!(
7400        with_host(|h| h.get(built).cloned()),
7401        Some(JsObj::Array(_))
7402            | Some(JsObj::Map { .. })
7403            | Some(JsObj::Set { .. })
7404            | Some(JsObj::RegExp(_))
7405            | Some(JsObj::Promise { .. })
7406            | Some(JsObj::Func(_))
7407            | Some(JsObj::Str(_))
7408            | Some(JsObj::BigInt(_))
7409            | Some(JsObj::Symbol { .. })
7410    );
7411    if !exotic {
7412        return false;
7413    }
7414    let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7415        return false;
7416    };
7417    let (dst, src) = (*dst, *src);
7418    with_host(|h| {
7419        if let Some(obj) = h.get(built).cloned() {
7420            if let Some(slot) = h.get_mut(inst) {
7421                *slot = obj;
7422            }
7423        }
7424        h.move_index_state(src, dst);
7425    });
7426    true
7427}
7428
7429fn adopt_own_props(inst: &Value, built: &Value) {
7430    let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7431        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7432        _ => Vec::new(),
7433    });
7434    with_host(|h| {
7435        let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7436        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7437            for (k, v) in entries {
7438                props.insert(k, v);
7439            }
7440            canonicalize_own_keys(props);
7441        }
7442        // The copied slots keep the attributes the source gave them, so
7443        // `class E extends Error` instances hide `message`/`stack` too.
7444        for k in keys {
7445            let a = h.prop_attrs(built, &k);
7446            h.set_prop_attrs(inst, &k, a);
7447        }
7448    });
7449}
7450
7451// ── class construction (runtime) ─────────────────────────────────────────────
7452
7453/// Build a class constructor value from its parts. The compiler emits (via
7454/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7455/// undefined for a default constructor); methods/getters/setters/statics/fields
7456/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7457pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7458    // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7459    // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7460    // trap and so re-enters the host, which the borrow below cannot allow.
7461    // Without it the link fell back to `Object.prototype` and every inherited
7462    // method went missing.
7463    let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7464        .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7465        .flatten();
7466    with_host(|h| {
7467        let parent_opt = if matches!(parent, Value::Undef) {
7468            None
7469        } else {
7470            Some(parent.clone())
7471        };
7472        // The class prototype delegates to the parent's prototype (or
7473        // Object.prototype for a base class). Extending a builtin error links to
7474        // that error's prototype so `instanceof Error` holds for the subclass.
7475        let parent_proto = match &parent_opt {
7476            Some(_) if proxy_parent_proto.is_some() => {
7477                proxy_parent_proto.clone().expect("checked is_some")
7478            }
7479            Some(p) => match h.get(p).cloned() {
7480                Some(JsObj::Class(pc)) => pc.proto.clone(),
7481                Some(JsObj::Builtin(bn)) => {
7482                    h.ensure_error_protos();
7483                    h.ensure_native_protos();
7484                    // `class S extends String {}` links to the REAL
7485                    // `String.prototype`, the same way an error subclass links
7486                    // to its error prototype. Without it `S.prototype`'s
7487                    // `[[Prototype]]` fell back to `Object.prototype`, so
7488                    // `new S("hi") instanceof String` read false and
7489                    // `String(new S("hi"))` reported `[object String]` instead
7490                    // of `hi`.
7491                    error_proto_of(h, &bn)
7492                        .or_else(|| h.native_proto(&bn))
7493                        .or_else(|| h.fn_prop(p, "prototype"))
7494                        .unwrap_or_else(|| h.object_proto())
7495                }
7496                _ => h
7497                    .fn_prop(p, "prototype")
7498                    .unwrap_or_else(|| h.object_proto()),
7499            },
7500            None => h.object_proto(),
7501        };
7502        let proto = h.new_object(IndexMap::new());
7503        h.set_proto(&proto, parent_proto);
7504        let ctor_opt = if matches!(ctor, Value::Undef) {
7505            None
7506        } else {
7507            Some(ctor.clone())
7508        };
7509        // Give the constructor closure its home class (for `super.method()`), and
7510        // record its `.name`.
7511        if let Some(cf) = &ctor_opt {
7512            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7513                f.home_class = Some(name.to_string());
7514            }
7515        }
7516        let cval = ClassVal {
7517            name: name.to_string(),
7518            ctor: ctor_opt,
7519            parent: parent_opt,
7520            proto: proto.clone(),
7521            statics: IndexMap::new(),
7522            fields: Vec::new(),
7523            source_def,
7524        };
7525        let class_val = h.alloc(JsObj::Class(cval));
7526        h.class_registry.insert(name.to_string(), class_val.clone());
7527        // Link prototype → class (for instance display + `constructor`), and give
7528        // the class its own `prototype` fn-prop so `C.prototype` reads work.
7529        h.tag_proto_class(&proto, class_val.clone());
7530        h.set_fn_prop(&class_val, "prototype", proto.clone());
7531        // `Class.prototype.constructor === Class`.
7532        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7533            p.insert("constructor".to_string(), class_val.clone());
7534        }
7535        h.hide_prop(&proto, "constructor");
7536        class_val
7537    })
7538}
7539
7540/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7541/// `member::*` tag; `is_static` targets the constructor side.
7542pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7543    with_host(|h| {
7544        let cname = match h.get(class_val) {
7545            Some(JsObj::Class(c)) => c.name.clone(),
7546            _ => String::new(),
7547        };
7548        // A private method/accessor: remember which class declared it, so a
7549        // brand-check failure can name the class the way node does. A static
7550        // FIELD is data, not a method, so it keeps the field wording.
7551        if name.starts_with('#') && kind != member::STATIC_FIELD {
7552            h.note_private_method(name);
7553        }
7554        // Give the method its home class for `super.x()`, and record whether it
7555        // is static — `super` resolves against a different object either way.
7556        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7557            f.home_class = Some(cname);
7558            f.home_static = is_static;
7559        }
7560        // Static members live on the constructor (fn-props / static accessors);
7561        // instance members on the prototype.
7562        let target = if is_static {
7563            class_val.clone()
7564        } else {
7565            match h.get(class_val) {
7566                Some(JsObj::Class(c)) => c.proto.clone(),
7567                _ => return,
7568            }
7569        };
7570        match kind {
7571            member::GET => h.set_accessor(&target, name, Some(func), None),
7572            member::SET => h.set_accessor(&target, name, None, Some(func)),
7573            _ => {
7574                // A static field is enumerable (`Object.keys(C)` lists it) unlike
7575                // a method, so it must not reach the `hide_prop` below.
7576                if kind == member::STATIC_FIELD {
7577                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7578                        c.statics.insert(name.to_string(), func.clone());
7579                    }
7580                    h.set_fn_prop(class_val, name, func);
7581                    return;
7582                }
7583                if is_static {
7584                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7585                        c.statics.insert(name.to_string(), func.clone());
7586                    }
7587                    h.set_fn_prop(class_val, name, func);
7588                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7589                    p.insert(name.to_string(), func);
7590                }
7591            }
7592        }
7593        // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7594        // Evaluation), so `for (k in instance)` walking the prototype chain never
7595        // yields them and `Object.keys(C.prototype)` is empty.
7596        h.hide_prop(&target, name);
7597    });
7598}
7599
7600/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7601pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7602    with_host(|h| {
7603        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7604            c.fields.push((name.to_string(), thunk, name_anon));
7605        }
7606    });
7607}
7608
7609/// The `[[Prototype]]` object a constructor value hands to its instances
7610/// (`Ctor.prototype`), for `instanceof`.
7611fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7612    match h.get(ctor) {
7613        Some(JsObj::Class(c)) => Some(c.proto.clone()),
7614        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7615        // A builtin's prototype lives in one of two registries: the error
7616        // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7617        // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7618        // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7619        // `Buffer.prototype instanceof Uint8Array` read false even though the
7620        // link was there — the instance case only passed via a native-tag
7621        // special case, which a prototype object does not carry.
7622        Some(JsObj::Builtin(name)) => h
7623            .error_protos
7624            .get(name)
7625            .or_else(|| h.native_protos.get(name))
7626            .cloned(),
7627        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7628        _ => None,
7629    }
7630}
7631
7632/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7633/// so a chain walk driven by that function can compare the two with `strict_eq`.
7634///
7635/// `ctor_prototype` answers only for the constructors whose prototype object
7636/// really exists on the heap (classes, user functions, the error and native
7637/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7638/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7639/// mirrors that fallback rather than reporting "no prototype" and failing every
7640/// comparison.
7641fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7642    if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7643        return Some(p);
7644    }
7645    let name = with_host(|h| match h.get(ctor) {
7646        Some(JsObj::Builtin(n)) => Some(n.clone()),
7647        _ => None,
7648    })?;
7649    if name == "Object" {
7650        return Some(with_host(|h| h.object_proto()));
7651    }
7652    Some(with_host(|h| {
7653        h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7654    }))
7655}
7656
7657/// V8's "not a function" wording for a value that was expected to be callable.
7658/// A number/string/boolean is named WITH its value (`number 1 is not a
7659/// function`, `string "s" is not a function`); every other type is named by type
7660/// alone (`object is not a function`, `symbol is not a function`).
7661pub fn not_a_function_message(v: &Value) -> String {
7662    with_host(|h| match v {
7663        Value::Undef => "undefined is not a function".into(),
7664        Value::Bool(b) => format!("boolean {b} is not a function"),
7665        Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7666        Value::Str(s) => format!("string \"{s}\" is not a function"),
7667        Value::Obj(_) => match h.get(v) {
7668            Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7669            Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7670            Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7671            _ => "object is not a function".into(),
7672        },
7673        _ => "object is not a function".into(),
7674    })
7675}
7676
7677/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7678/// `ctor.prototype`.
7679pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7680    // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7681    // right-hand side REPLACES the prototype-chain walk entirely, and it is
7682    // consulted before the callability check — which is why a plain (uncallable)
7683    // object that defines it is a legal `instanceof` right-hand side.
7684    if matches!(ctor, Value::Obj(_)) {
7685        // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7686        // plain function both land in the fn-prop side table (which
7687        // `class_static` reads, following the `extends` chain), NOT in an object
7688        // property map — so consulting only `lookup_chain` would find the object
7689        // literal form and silently miss the two forms V8 users actually write.
7690        let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7691            Some(f) => Some(f),
7692            None => protocol_lookup(ctor, "@@hasInstance")?,
7693        };
7694        // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7695        // else that is not callable is a TypeError, so a data property here does
7696        // NOT fall back to the prototype walk.
7697        match handler {
7698            Some(f) if with_host(|h| is_callable(h, &f)) => {
7699                let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7700                return Ok(with_host(|h| h.truthy(&r)));
7701            }
7702            Some(f)
7703                if !matches!(f, Value::Undef)
7704                    && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7705            {
7706                return Err(type_error(&not_a_function_message(&f)));
7707            }
7708            _ => {}
7709        }
7710    }
7711    // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7712    // `1 instanceof 3` throws even though the left side could never match.
7713    // Returning early on the left side skipped that check entirely.
7714    let ctor_callable = with_host(|h| {
7715        matches!(
7716            h.get(ctor),
7717            Some(JsObj::Func(_))
7718                | Some(JsObj::Class(_))
7719                | Some(JsObj::Builtin(_))
7720                | Some(JsObj::BoundFunc { .. })
7721        )
7722    });
7723    if !ctor_callable {
7724        // V8 has TWO messages here and they are not interchangeable: a primitive
7725        // right-hand side is "not an object", an object that is merely not
7726        // callable is "not callable". Only the second was implemented, so
7727        // `1 instanceof 3` reported nothing at all.
7728        return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7729            "Right-hand side of 'instanceof' is not callable"
7730        } else {
7731            "Right-hand side of 'instanceof' is not an object"
7732        }));
7733    }
7734    // A non-object left-hand side is never an instance — but only after the
7735    // right-hand side has been validated above.
7736    if !matches!(obj, Value::Obj(_)) {
7737        return Ok(false);
7738    }
7739    // A Proxy shares no heap variant with its target, so the structural arms
7740    // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7741    // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7742    // walk here, which also gives a custom trap the final say.
7743    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7744        with_host(|h| {
7745            h.ensure_error_protos();
7746            h.ensure_native_protos();
7747        });
7748        let Some(target) = walk_target_prototype(ctor) else {
7749            return Ok(false);
7750        };
7751        let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7752        for _ in 0..100 {
7753            if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7754                return Ok(false);
7755            }
7756            if with_host(|h| h.strict_eq(&cur, &target)) {
7757                return Ok(true);
7758            }
7759            cur = crate::builtins::prototype_of(&cur);
7760        }
7761        return Ok(false);
7762    }
7763    // Builtin constructors whose instances aren't prototype-linked in our model
7764    // (arrays/plain objects/functions) get a structural instanceof.
7765    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7766        // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7767        // an instance regardless of its own kind, which is the whole of the ES5
7768        // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7769        // makes `new F() instanceof Array` true. A structural test alone said
7770        // false.
7771        if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7772            return Ok(true);
7773        }
7774        let kind = with_host(|h| h.get(obj).cloned());
7775        match name.as_str() {
7776            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7777            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7778            // Map/Set/Promise instances are distinct heap variants, not
7779            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7780            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7781            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7782            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7783            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7784            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7785            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7786            // A RegExp is its own heap variant too, not a prototype-linked object.
7787            "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7788            "Object" => {
7789                // Everything object-typed except a null-prototype object is an
7790                // Object instance.
7791                let is_obj = matches!(
7792                    kind,
7793                    Some(JsObj::Object(_))
7794                        | Some(JsObj::Array(_))
7795                        // A namespace object and a builtin function are both
7796                        // `instanceof Object`: `Math instanceof Object` is true.
7797                        | Some(JsObj::Builtin(_))
7798                        | Some(JsObj::Func(_))
7799                        | Some(JsObj::Class(_))
7800                        | Some(JsObj::Map { .. })
7801                        | Some(JsObj::Set { .. })
7802                        | Some(JsObj::Promise { .. })
7803                        | Some(JsObj::Generator { .. })
7804                        | Some(JsObj::RegExp(_))
7805                );
7806                if is_obj {
7807                    // A null-prototype object (Object.create(null) or
7808                    // setPrototypeOf(o, null)) is NOT an Object instance.
7809                    if with_host(|h| h.has_null_proto(obj)) {
7810                        return Ok(false);
7811                    }
7812                    return Ok(true);
7813                }
7814                return Ok(false);
7815            }
7816            // A Node `Buffer` IS a `Uint8Array` subclass instance.
7817            "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7818                return Ok(true);
7819            }
7820            // Every typed array carries the same `TypedArray` tag; the constructor
7821            // it is an instance of is its ELEMENT KIND.
7822            k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7823                return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7824            }
7825            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7826            // `TextEncoder`, …) is an instance of the builtin whose name matches
7827            // its hidden `@@native` tag.
7828            other => {
7829                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7830                    return Ok(true);
7831                }
7832            }
7833        }
7834    }
7835    with_host(|h| h.ensure_error_protos());
7836    // The native exotic prototypes are built lazily; `instanceof` may be the
7837    // first thing to ask for them, so materialise them before the chain walk.
7838    with_host(|h| h.ensure_native_protos());
7839    let target = match with_host(|h| ctor_prototype(h, ctor)) {
7840        Some(p) => p,
7841        None => return Ok(false),
7842    };
7843    let mut cur = with_host(|h| h.proto_of(obj));
7844    while let Some(p) = cur {
7845        if with_host(|h| h.strict_eq(&p, &target)) {
7846            return Ok(true);
7847        }
7848        cur = with_host(|h| h.proto_of(&p));
7849    }
7850    Ok(false)
7851}
7852
7853// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7854
7855impl JsHost {
7856    /// Swap the volatile execution context in one shot, returning the previous
7857    /// one — installs a generator's context on resume, pulls it back on suspend.
7858    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7859        std::mem::swap(&mut self.frames, &mut c.frames);
7860        std::mem::swap(&mut self.error, &mut c.error);
7861        std::mem::swap(&mut self.exc, &mut c.exc);
7862        std::mem::swap(&mut self.signal, &mut c.signal);
7863        c
7864    }
7865    pub fn is_generator_val(&self, v: &Value) -> bool {
7866        matches!(self.get(v), Some(JsObj::Generator { .. }))
7867    }
7868    /// Whether `v` is an ASYNC generator object — the borrow-free form of
7869    /// [`is_async_generator`], usable from code already holding the host.
7870    pub fn is_async_gen_val(&self, v: &Value) -> bool {
7871        match self.get(v) {
7872            Some(JsObj::Generator { id }) => self
7873                .generators
7874                .get(*id as usize)
7875                .map(|g| g.async_gen)
7876                .unwrap_or(false),
7877            _ => false,
7878        }
7879    }
7880    pub fn gen_done(&self, id: u32) -> bool {
7881        self.generators
7882            .get(id as usize)
7883            .map(|g| g.done)
7884            .unwrap_or(true)
7885    }
7886    fn gen_started(&self, id: u32) -> bool {
7887        self.generators
7888            .get(id as usize)
7889            .map(|g| g.started)
7890            .unwrap_or(false)
7891    }
7892}
7893
7894/// Build a suspended generator whose body is `chunk`, run in a frame with the
7895/// already-bound `env`. Nothing executes until the first `gen_resume`.
7896fn make_generator(
7897    chunk: Chunk,
7898    env: Env,
7899    this_val: Option<Value>,
7900    home_class: Option<String>,
7901    home_static: bool,
7902    home_object: Option<Value>,
7903    strict: bool,
7904) -> Value {
7905    let home = home_class
7906        .as_ref()
7907        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7908    let frame = Frame {
7909        base_env: env.clone(),
7910        env,
7911        this_obj: this_val,
7912        new_target: None,
7913        home_class: home,
7914        home_static,
7915        home_object,
7916        strict,
7917        line: 0,
7918        owner: None,
7919        is_module: false,
7920        this_state: ThisState::Plain,
7921    };
7922    let id = with_host(|h| {
7923        let id = h.generators.len() as u32;
7924        h.generators.push(GenCell {
7925            coro: None,
7926            yielder: std::ptr::null(),
7927            ctx: GenContext {
7928                frames: vec![frame],
7929                ..GenContext::default()
7930            },
7931            done: false,
7932            started: false,
7933            inject: None,
7934            async_gen: false,
7935            queue: std::collections::VecDeque::new(),
7936            running: false,
7937            stack_floor: 0,
7938        });
7939        id
7940    });
7941    let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7942        ensure_coroutine_floor();
7943        // Same thread → publish the yielder so `yield` (deep in the body's VM)
7944        // can reach it. Valid for the whole body lifetime.
7945        with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7946        let r = run_chunk_on(chunk);
7947        // A `return` inside the body leaves a Return signal carrying the final
7948        // value; capture it so `.next()` reports it as the completion value.
7949        let ret = with_host(|h| match h.signal.take() {
7950            Some(Signal::Return(v)) => v,
7951            _ => Value::Undef,
7952        });
7953        r.map(|_| ret)
7954    };
7955    // The body's stack is allocated here rather than left to `Coroutine::new` so
7956    // that its size is ours to choose and, above all, so its `limit()` is known:
7957    // that address is what `stack_exhausted` must compare against while the body
7958    // runs, since a coroutine does NOT run on the thread stack pthread reports.
7959    // A refused reservation still yields a working generator on corosensei's own
7960    // 1 MiB default, with a floor derived on entry instead.
7961    let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7962        Ok(stack) => {
7963            let floor = coro_stack_floor(&stack);
7964            (corosensei::Coroutine::with_stack(stack, body), floor)
7965        }
7966        Err(_) => (corosensei::Coroutine::new(body), 0),
7967    };
7968    with_host(|h| {
7969        h.generators[id as usize].coro = Some(coro);
7970        h.generators[id as usize].stack_floor = floor;
7971    });
7972    with_host(|h| h.alloc(JsObj::Generator { id }))
7973}
7974
7975/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7976/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7977pub fn gen_yield(v: Value) -> Result<Value, String> {
7978    let id = match CUR_GEN.with(|c| c.get()) {
7979        Some(id) => id,
7980        None => return Err(type_error("yield outside a generator")),
7981    };
7982    let yp = with_host(|h| h.generators[id as usize].yielder);
7983    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7984    // only reach here from inside that body (its stack is live).
7985    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7986    let sent = yielder.suspend(v);
7987    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7988    // convert it into a Return signal / thrown value so the body unwinds and any
7989    // `finally` runs, exactly as a source-level `return`/`throw` would.
7990    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7991        match inj {
7992            GenInject::Return(rv) => {
7993                with_host(|h| h.signal = Some(Signal::Return(rv)));
7994                return Ok(Value::Undef);
7995            }
7996            GenInject::Throw(ev) => {
7997                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7998                with_host(|h| h.exc = Some(ev));
7999                return Err(msg);
8000            }
8001        }
8002    }
8003    Ok(sent)
8004}
8005
8006/// `generator.return(v)`: force the generator to complete, running any pending
8007/// `finally`. If it is already done (or never started) it just reports
8008/// `{value:v, done:true}` without executing the body.
8009pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
8010    let id = match with_host(|h| h.get(gen).cloned()) {
8011        Some(JsObj::Generator { id }) => id,
8012        _ => return Err(type_error("not a generator")),
8013    };
8014    // Not started yet (coro present, ctx never resumed) OR already done → no body
8015    // to unwind: complete immediately with the supplied value.
8016    let started = with_host(|h| h.gen_started(id));
8017    if with_host(|h| h.generators[id as usize].done) || !started {
8018        with_host(|h| h.generators[id as usize].done = true);
8019        return Ok(GenStep::Done(v));
8020    }
8021    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
8022    gen_resume(gen, Value::Undef)
8023}
8024
8025/// `generator.throw(e)`: inject a throw at the suspension point, running any
8026/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
8027pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
8028    let id = match with_host(|h| h.get(gen).cloned()) {
8029        Some(JsObj::Generator { id }) => id,
8030        _ => return Err(type_error("not a generator")),
8031    };
8032    let started = with_host(|h| h.gen_started(id));
8033    if with_host(|h| h.generators[id as usize].done) || !started {
8034        // A throw into a done/unstarted generator propagates to the caller.
8035        with_host(|h| h.generators[id as usize].done = true);
8036        let msg = with_host(|h| crate::builtins::error_string(h, &e));
8037        with_host(|h| h.exc = Some(e));
8038        return Err(msg);
8039    }
8040    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
8041    gen_resume(gen, Value::Undef)
8042}
8043
8044/// Outcome of resuming a generator: a yielded value (not done), or the final
8045/// completion value (done).
8046pub enum GenStep {
8047    Yield(Value),
8048    Done(Value),
8049}
8050
8051/// Resume a generator until its next `yield` or its body returns. Preserves the
8052/// shared host: the coroutine is taken out so the body re-enters `with_host`
8053/// freely, and the volatile context is swapped so the caller's frames/signal
8054/// survive the switch.
8055pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8056    let id = match with_host(|h| h.get(gen).cloned()) {
8057        Some(JsObj::Generator { id }) => id,
8058        _ => return Err(type_error("not a generator")),
8059    };
8060    if with_host(|h| h.generators[id as usize].done) {
8061        return Ok(GenStep::Done(Value::Undef));
8062    }
8063    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8064        Some(c) => c,
8065        None => return Err("TypeError: generator already executing".into()),
8066    };
8067    with_host(|h| h.generators[id as usize].started = true);
8068    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8069    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8070    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8071    // The body runs on the coroutine's OWN stack, so the guard's floor has to
8072    // move with it and move back on suspend — generators nest, and a resume from
8073    // inside another generator must restore that one's floor, not the thread's.
8074    let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8075    let caller_floor = swap_stack_floor(coro_floor);
8076
8077    let out = coro.resume(send); // no host borrow held; body drives its own VM
8078
8079    let measured = swap_stack_floor(caller_floor);
8080    // A coroutine on corosensei's default stack has no known bounds, so the
8081    // floor it measured for itself on first entry is kept for later resumes.
8082    if coro_floor == 0 && measured != 0 {
8083        with_host(|h| h.generators[id as usize].stack_floor = measured);
8084    }
8085    CUR_GEN.with(|c| c.set(prev));
8086    let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8087    // A `throw` inside the body left the thrown VALUE in the generator's context,
8088    // which the swap above just stashed away. Hand it to the caller so the
8089    // rejection/catch keeps the original error object instead of a string rebuild.
8090    let thrown = gen_ctx.exc.take();
8091    with_host(|h| {
8092        if let Some(v) = thrown {
8093            h.exc = Some(v);
8094        }
8095        h.generators[id as usize].ctx = gen_ctx;
8096        h.generators[id as usize].coro = Some(coro);
8097    });
8098
8099    match out {
8100        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8101        corosensei::CoroutineResult::Return(r) => {
8102            // Release the coroutine — and with it the mmap'd stack it owns —
8103            // the moment the body completes. `h.generators` only ever grows (an
8104            // id is never reused), so a program that awaits in a loop otherwise
8105            // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8106            // the life of the process. A finished generator is never resumed:
8107            // `gen_resume` returns `Done` on the `done` flag before it looks.
8108            with_host(|h| {
8109                let g = &mut h.generators[id as usize];
8110                g.done = true;
8111                g.coro = None;
8112            });
8113            match r {
8114                Ok(v) => Ok(GenStep::Done(v)),
8115                Err(e) => Err(e),
8116            }
8117        }
8118    }
8119}
8120
8121/// Force a generator to completion (used by `.return()` and abandoned loops):
8122/// marks it done without running further.
8123pub fn gen_close(gen: &Value) {
8124    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8125        with_host(|h| h.generators[id as usize].done = true);
8126    }
8127}
8128
8129// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8130
8131/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8132pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8133    match v {
8134        Value::Undef => MapKey::Undef,
8135        Value::Bool(b) => MapKey::Bool(*b),
8136        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8137        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8138        Value::Str(s) => MapKey::Str((**s).clone()),
8139        Value::Obj(i) => match h.get(v) {
8140            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8141            Some(JsObj::Null) => MapKey::Null,
8142            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8143            Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8144            _ => MapKey::Ref(*i),
8145        },
8146        _ => MapKey::Undef,
8147    }
8148}
8149
8150/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8151fn norm_num_bits(f: f64) -> u64 {
8152    if f.is_nan() {
8153        return f64::NAN.to_bits();
8154    }
8155    if f == 0.0 {
8156        return 0.0f64.to_bits(); // fold -0 into +0
8157    }
8158    f.to_bits()
8159}
8160
8161/// Fully materialize any iterable into a vector of values.
8162/// Pull at most `n` values, then close the iterator — 8.6.2
8163/// IteratorBindingInitialization, which is what an array destructuring pattern
8164/// without a `...rest` element performs.
8165///
8166/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8167/// fixed number of targets, so the spec pulls exactly that many and calls
8168/// IteratorClose on whatever is left; draining instead made
8169///
8170/// ```text
8171/// const [first] = infiniteGenerator();
8172/// ```
8173///
8174/// run forever. It is also observable on any finite iterator, as the count of
8175/// `next()` calls and whether `return()` ever ran.
8176///
8177/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8178/// using `iter_all` and an unbounded source hangs there in node too.
8179pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8180    // A Proxy iterates through its traps, which materialize eagerly; there is
8181    // no step-wise form to bound, so this keeps the draining behaviour.
8182    if let Some(items) = crate::proxy::iterate(v)? {
8183        return Ok(items.into_iter().take(n).collect());
8184    }
8185    if with_host(|h| h.is_generator_val(v)) {
8186        let mut out = Vec::new();
8187        while out.len() < n {
8188            match gen_resume(v, Value::Undef)? {
8189                GenStep::Yield(x) => out.push(x),
8190                _ => return Ok(out), // ran out on its own; nothing left to close
8191            }
8192        }
8193        // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8194        let _ = gen_return(v, Value::Undef);
8195        return Ok(out);
8196    }
8197    if let Some(iter_fn) = user_iterator_fn(v) {
8198        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8199        let mut out = Vec::new();
8200        while out.len() < n {
8201            let step = call_method(&iterator, "next", Vec::new())?;
8202            // Read first: resolving the property re-enters the host, so doing
8203            // it inside the `with_host` closure double-borrows and aborts.
8204            let done = get_prop_chain(&step, "done")?;
8205            if with_host(|h| h.truthy(&done)) {
8206                return Ok(out);
8207            }
8208            out.push(get_prop_chain(&step, "value")?);
8209        }
8210        // IteratorClose: `return` is optional on the protocol, and a throw from
8211        // it is swallowed here the way a normal (non-abrupt) completion does.
8212        if let Ok(ret) = get_prop_chain(&iterator, "return") {
8213            if with_host(|h| is_callable(h, &ret)) {
8214                let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8215            }
8216        }
8217        return Ok(out);
8218    }
8219    // Arrays, strings, Map/Set: already materialized, and their built-in
8220    // iterators carry no `return`, so there is nothing to close. The same
8221    // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8222    // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8223    // longer carried `Symbol.iterator`.
8224    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8225        let shown = with_host(|h| h.inspect(v));
8226        return Err(type_error(&format!("{shown} is not iterable")));
8227    }
8228    with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8229}
8230
8231pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8232    // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8233    // no heap variant `iter_vec` could recognise.
8234    if let Some(items) = crate::proxy::iterate(v)? {
8235        return Ok(items);
8236    }
8237    // Generators / user iterators must resume without a live host borrow.
8238    if with_host(|h| h.is_generator_val(v)) {
8239        let mut out = Vec::new();
8240        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8241            out.push(x);
8242        }
8243        return Ok(out);
8244    }
8245    // A live array iterator is stepped to the end, so an accessor or a hole
8246    // reads as `a[i]` does and the iterator is left exhausted, as spreading it
8247    // leaves it in node.
8248    if with_host(|h| matches!(h.get(v), Some(JsObj::Iter { array: Some(_), .. }))) {
8249        let mut out = Vec::new();
8250        while let Some(Some(x)) = crate::builtins::iter_step(v) {
8251            out.push(x);
8252        }
8253        return Ok(out);
8254    }
8255    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8256    // Checked BEFORE the reachability guard below, since an own `Symbol
8257    // .iterator` makes a value iterable no matter what its prototype is.
8258    if let Some(iter_fn) = user_iterator_fn(v) {
8259        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8260        return drain_iterator(&iterator);
8261    }
8262    // The fast paths below read a builtin's backing storage directly, which is
8263    // only legitimate while that builtin's `Symbol.iterator` is still
8264    // reachable: replacing the prototype takes it away, and node then reports
8265    // the value as not iterable. Spread, destructuring and `Array.from`'s
8266    // iterable branch all funnel through here.
8267    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8268        let shown = with_host(|h| h.inspect(v));
8269        return Err(type_error(&format!("{shown} is not iterable")));
8270    }
8271    // A String wrapper iterates its code POINTS, exactly as the primitive does
8272    // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8273    if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8274        if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8275            return iter_all(&prim);
8276        }
8277    }
8278    // An array's index ACCESSORS are not in its backing vector, so iterating one
8279    // (spread, `for-of`, `Array.from`) has to resolve them the way the
8280    // `Array.prototype` methods do.
8281    let mut items = with_host(|h| h.iter_vec(v))?;
8282    if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8283        crate::builtins::resolve_index_accessors_pub(v, &mut items);
8284    }
8285    Ok(items)
8286}
8287
8288// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8289
8290/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8291/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8292/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8293/// are awaited one at a time by `async_step`.
8294pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8295    if let Some(f) = user_async_iterator_fn(src) {
8296        return invoke(&f, Vec::new(), Some(src.clone()));
8297    }
8298    // An `async function*` object IS its own async iterator; draining it into a
8299    // list here would run the whole body (and any `finally`) before the consumer
8300    // sees the first value.
8301    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8302        if with_host(|h| h.generators[id as usize].async_gen) {
8303            return Ok(src.clone());
8304        }
8305    }
8306    let items = iter_all(src)?;
8307    Ok(with_host(|h| {
8308        h.alloc(JsObj::Iter {
8309            items,
8310            idx: 0,
8311            array: None,
8312        })
8313    }))
8314}
8315
8316/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8317fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8318    // A PROXY supplies the protocol through its `get` trap and is not a plain
8319    // object, so the shape test below rejects it outright.
8320    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8321        return protocol_lookup(v, "@@asyncIterator")
8322            .ok()
8323            .flatten()
8324            .filter(|f| with_host(|h| is_callable(h, f)));
8325    }
8326    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8327    if !is_plain {
8328        return None;
8329    }
8330    // Full property resolution, not a stored-property lookup — the same reason
8331    // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8332    // dispatches its methods through the stdlib method table rather than a
8333    // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8334    // even though reading it gives a function: `for await (const v of
8335    // timersPromises.setInterval(…))` said the iterator "is not iterable".
8336    let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8337    with_host(|h| is_callable(h, &f)).then_some(f)
8338}
8339
8340/// One step of a `for await` loop: return a Promise that settles to a
8341/// `{value, done}` record. For a native async iterator this is `iter.next()`
8342/// (already a promise of the record). For the sync fallback it pops the next raw
8343/// value, awaits it, and packages `{value: resolved, done:false}` (or
8344/// `{done:true}` at exhaustion).
8345pub fn async_step(iterator: &Value) -> Result<Value, String> {
8346    // An `async function*` object: resume it through the await-aware driver.
8347    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8348        if with_host(|h| h.generators[id as usize].async_gen) {
8349            return Ok(async_gen_step(iterator, Value::Undef));
8350        }
8351    }
8352    // Sync-fallback iterator: drive it here, awaiting each yielded value.
8353    if let Some(JsObj::Iter { items, idx, .. }) = with_host(|h| h.get(iterator).cloned()) {
8354        if idx >= items.len() {
8355            // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8356            // promise even at exhaustion, so the `done: true` step costs the same
8357            // two microtask ticks a value step does.
8358            let step = with_host(|h| h.new_promise());
8359            let sid = with_host(|h| h.promise_id(&step).unwrap());
8360            with_host(|h| {
8361                h.queue_micro_native(Box::new(move || {
8362                    resolve_promise_val(sid, iter_record(Value::Undef, true));
8363                    Ok(())
8364                }))
8365            });
8366            return Ok(step);
8367        }
8368        let raw = items[idx].clone();
8369        with_host(|h| {
8370            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8371                *idx += 1;
8372            }
8373        });
8374        // Await the raw value (adopts a promise's resolution), then wrap.
8375        let step = with_host(|h| h.new_promise());
8376        let sid = with_host(|h| h.promise_id(&step).unwrap());
8377        let raw_p = promise_of(&raw);
8378        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8379        subscribe_native(
8380            raw_id,
8381            Box::new(move |state, val| {
8382                if state == PromiseState::Rejected {
8383                    reject_promise_val(sid, val);
8384                } else {
8385                    resolve_promise_val(sid, iter_record(val, false));
8386                }
8387                Ok(())
8388            }),
8389        );
8390        return Ok(step);
8391    }
8392    // Native async iterator: `iter.next()` returns the {value,done} promise.
8393    let r = call_method(iterator, "next", Vec::new())?;
8394    Ok(promise_of(&r))
8395}
8396
8397/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8398/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8399pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8400    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8401    if !is_plain {
8402        return None;
8403    }
8404    // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8405    // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8406    // stdlib method table rather than a property map, so `lookup_chain` reported
8407    // no `Symbol.iterator` for one even though reading it gave a function —
8408    // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8409    let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8410    with_host(|h| is_callable(h, &f)).then_some(f)
8411}
8412
8413/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8414/// exhaustion.
8415/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8416/// `f` exits abruptly (7.4.9 IteratorClose).
8417///
8418/// The difference from `iter_all` + a loop is that this never materializes the
8419/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8420/// stop at the first call, and draining first means it never gets there at all.
8421pub fn iter_for_each(
8422    src: &Value,
8423    mut f: impl FnMut(Value, usize) -> Result<(), String>,
8424) -> Result<(), String> {
8425    // Only a USER iterator can be infinite or observe its own close; every
8426    // other shape is already a finite materialized sequence.
8427    let Some(iter_fn) = user_iterator_fn(src) else {
8428        for (i, v) in iter_all(src)?.into_iter().enumerate() {
8429            f(v, i)?;
8430        }
8431        return Ok(());
8432    };
8433    let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8434    let mut i = 0usize;
8435    loop {
8436        let step = call_method(&iterator, "next", Vec::new())?;
8437        let done = get_prop_chain(&step, "done")?;
8438        if with_host(|h| h.truthy(&done)) {
8439            return Ok(());
8440        }
8441        let value = get_prop_chain(&step, "value")?;
8442        if let Err(e) = f(value, i) {
8443            // The callback's error wins over anything `return()` raises, so a
8444            // throwing `return` is swallowed here (7.4.9 step 6).
8445            let _ = close_iterator(&iterator);
8446            return Err(e);
8447        }
8448        i += 1;
8449    }
8450}
8451
8452/// Call `iterator.return()` if it has one, as IteratorClose does.
8453pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8454    let has = crate::builtins::get_property(iterator, "return")?;
8455    if with_host(|h| is_callable(h, &has)) {
8456        call_method(iterator, "return", Vec::new())?;
8457    }
8458    Ok(())
8459}
8460
8461pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8462    let mut out = Vec::new();
8463    loop {
8464        let step = call_method(iterator, "next", Vec::new())?;
8465        let done = get_prop_chain(&step, "done")?;
8466        if with_host(|h| h.truthy(&done)) {
8467            break;
8468        }
8469        out.push(get_prop_chain(&step, "value")?);
8470    }
8471    Ok(out)
8472}
8473
8474/// Property read that walks the prototype chain (used by iteration helpers).
8475/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8476/// `Math.floor`); the exceptions are the namespace objects a script can only
8477/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8478/// report `typeof === "object"` and carry no `name`/`length`.
8479pub fn builtin_is_callable(n: &str) -> bool {
8480    // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8481    // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8482    // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8483    // `true` to `instanceof Function`. `Function.prototype` is the one that
8484    // really IS callable (10.2.4: it is an anonymous built-in that returns
8485    // undefined), which is why it is not stripped here.
8486    if n != "Function.prototype" && n.ends_with(".prototype") {
8487        return false;
8488    }
8489    // A `match` rather than a slice scan: this runs on every callability test,
8490    // which is every call and every `ToPrimitive`, and a `contains` over the
8491    // list below compares against all 56 entries before answering "callable" —
8492    // the common case. The compiler turns the arms into a length-then-bytes
8493    // decision tree instead.
8494    !matches!(
8495        n,
8496        "Math"
8497            | "JSON"
8498            | "console"
8499            | "Reflect"
8500            | "process"
8501            | "Atomics"
8502            | "performance"
8503            | "fs"
8504            | "path"
8505            | "os"
8506            | "util"
8507            | "crypto"
8508            | "webcrypto"
8509            | "SubtleCrypto"
8510            | "querystring"
8511            | "events"
8512            | "timers"
8513            | "perf_hooks"
8514            | "async_hooks"
8515            | "diagnostics_channel"
8516            | "v8"
8517            | "dns"
8518            | "punycode"
8519            | "child_process"
8520            | "tty"
8521            | "url"
8522            | "zlib"
8523            | "string_decoder"
8524            | "http"
8525            | "net"
8526            | "buffer"
8527            | "function"
8528            | "path/win32"
8529            | "fs/promises"
8530            | "stream/promises"
8531            | "stream/consumers"
8532            | "stream/web"
8533            | "timers/promises"
8534            | "dns/promises"
8535            | "https"
8536            | "http2"
8537            | "tls"
8538            | "dgram"
8539            | "cluster"
8540            | "worker_threads"
8541            | "readline"
8542            | "readline/promises"
8543            | "repl"
8544            | "vm"
8545            | "domain"
8546            | "trace_events"
8547            | "wasi"
8548            | "inspector"
8549            | "object"
8550    )
8551        // The live `require.cache` view is a plain object to a script, not
8552        // something it can call.
8553        && n != crate::builtins::REQUIRE_CACHE
8554}
8555
8556pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8557    crate::builtins::get_property(recv, name)
8558}
8559
8560/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8561/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8562/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8563/// native-tagged instances) is an object and must be converted.
8564pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8565    match v {
8566        Value::Obj(_) => matches!(
8567            h.get(v),
8568            None | Some(JsObj::Null)
8569                | Some(JsObj::Str(_))
8570                | Some(JsObj::Symbol { .. })
8571                | Some(JsObj::BigInt(_))
8572        ),
8573        _ => true,
8574    }
8575}
8576
8577/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8578/// or `"string"`.
8579///
8580/// An object carrying a `Symbol.toPrimitive` method (internal key
8581/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8582/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8583/// the order reversed for the string hint — and takes the FIRST call whose
8584/// result is a primitive. An object that yields no primitive (a null-prototype
8585/// object has neither method) throws V8's
8586/// `TypeError: Cannot convert object to primitive value`.
8587///
8588/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8589/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8590/// to read `str_of` directly and so never invoked a user `valueOf`.
8591pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8592    if with_host(|h| is_primitive(h, v)) {
8593        return Ok(v.clone());
8594    }
8595    if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8596        if with_host(|h| is_callable(h, &f)) {
8597            let hv = with_host(|h| h.new_str(hint.to_string()));
8598            let r = invoke(&f, vec![hv], Some(v.clone()))?;
8599            if with_host(|h| is_primitive(h, &r)) {
8600                return Ok(r);
8601            }
8602            return Err(type_error("Cannot convert object to primitive value"));
8603        }
8604    }
8605    // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8606    // `"string"`, which is why `new Date() + 1` concatenates while
8607    // `new Date() - 1` is arithmetic.
8608    let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8609        "string"
8610    } else {
8611        hint
8612    };
8613    let order = if hint == "string" {
8614        ["toString", "valueOf"]
8615    } else {
8616        ["valueOf", "toString"]
8617    };
8618    // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8619    // below is for exotics whose property funnel exposes no callable
8620    // `toString`, not for an object whose own methods ran and returned
8621    // non-primitives — that case is the spec's TypeError, and branding it
8622    // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8623    // quietly produced `"[object Object]1"`.
8624    let mut called_any = false;
8625    for m in order {
8626        let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8627        if !with_host(|h| is_callable(h, &f)) {
8628            continue;
8629        }
8630        called_any = true;
8631        // On a Proxy the resolved method is a thunk bound to the TARGET, so
8632        // invoking it directly would stringify the target — `String(new
8633        // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8634        // native-code form. `call_method` re-dispatches the generic
8635        // `Function.prototype`/`Object.prototype` methods against the proxy.
8636        let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8637            call_method(v, m, Vec::new())?
8638        } else {
8639            invoke(&f, Vec::new(), Some(v.clone()))?
8640        };
8641        if with_host(|h| is_primitive(h, &r)) {
8642            return Ok(r);
8643        }
8644    }
8645    // Every object except a null-prototype one inherits `Object.prototype
8646    // .toString`, which always returns a string — so the exhausted-methods
8647    // TypeError is reachable only there. The exotics whose property funnel has
8648    // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8649    // get the same `[object Tag]` brand V8 gives them.
8650    // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8651    // for both method names, and if what came back was not callable there is
8652    // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8653    // is a TypeError on node, where branding it `[object Object]` invented a
8654    // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8655    // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8656    // the target's `[object Map]` brand exactly as the bare `Map` does.
8657    if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8658        return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8659    }
8660    Err(type_error("Cannot convert object to primitive value"))
8661}
8662
8663/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8664/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8665/// is absent or returns an object — is invoked), then rendered by `str_of`.
8666/// Returns a heap string value.
8667pub fn to_string_value(v: &Value) -> Result<Value, String> {
8668    let p = to_primitive(v, "string")?;
8669    // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8670    // refuses. `String(sym)` is the documented exception and is handled at that
8671    // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8672    // `[sym].join()`) rejects the way node does instead of silently rendering
8673    // `Symbol(desc)`.
8674    if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8675        return Err(type_error("Cannot convert a Symbol value to a string"));
8676    }
8677    Ok(with_host(|h| {
8678        let s = h.str_of(&p);
8679        h.new_str(s)
8680    }))
8681}
8682
8683/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8684/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8685pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8686    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8687        return Ok(with_host(|h| {
8688            let s = h.str_of(v);
8689            h.new_str(s)
8690        }));
8691    }
8692    to_string_value(v)
8693}
8694
8695/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8696/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8697/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8698/// under the host borrow and so can never invoke a JS `valueOf`.
8699pub fn to_number_value(v: &Value) -> Result<f64, String> {
8700    // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8701    // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8702    // and `+Symbol()` are both `TypeError` on node v26.7.0.
8703    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8704        return Err(type_error("Cannot convert a Symbol value to a number"));
8705    }
8706    if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8707        return Ok(n);
8708    }
8709    let p = to_primitive(v, "number")?;
8710    // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8711    // the argument was checked, so an object whose conversion yields a symbol or
8712    // a BigInt slipped past: `+Object(9n)` answered 9 and
8713    // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8714    // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8715    // which is why `Number(Object(9n))` is still 9.
8716    match with_host(|h| h.get(&p).cloned()) {
8717        Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8718        Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8719        _ => Ok(with_host(|h| h.to_number(&p))),
8720    }
8721}
8722
8723/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8724/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8725/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8726pub fn to_property_key(v: &Value) -> Result<String, String> {
8727    // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8728    // `o[sym]`); only an object key pays for the conversion.
8729    if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8730        return Ok(k);
8731    }
8732    let p = to_primitive(v, "string")?;
8733    Ok(with_host(|h| h.str_of(&p)))
8734}
8735
8736/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8737/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8738/// target), so `typeof` and every `is_callable` guard agree on one answer.
8739pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8740    match h.get(v) {
8741        // Not every builtin is a function: the namespace objects (`Math`,
8742        // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8743        // calling one is a `TypeError` in node exactly as `typeof` says.
8744        Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8745        Some(JsObj::Func(_))
8746        | Some(JsObj::BoundMethod { .. })
8747        | Some(JsObj::BoundFunc { .. })
8748        | Some(JsObj::Class(_)) => true,
8749        Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8750        _ => false,
8751    }
8752}
8753
8754/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8755/// stored value (methods, inherited data props). Does NOT invoke accessors.
8756/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8757/// `then` and the rest — which the spec performs with `[[Get]]`.
8758///
8759/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8760/// map and never asks the handler, so a proxy supplying a protocol method
8761/// through its `get` trap was invisible and the operation fell back to the
8762/// default. Everything else takes the cheap chain walk.
8763pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8764    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8765        let got = crate::builtins::get_property(v, key)?;
8766        return Ok((!matches!(got, Value::Undef)).then_some(got));
8767    }
8768    Ok(with_host(|h| lookup_chain(h, v, key)))
8769}
8770
8771pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8772    if let Some(JsObj::Object(p)) = h.get(recv) {
8773        if let Some(v) = p.get(key) {
8774            return Some(v.clone());
8775        }
8776    }
8777    let mut cur = h.proto_of(recv);
8778    while let Some(p) = cur {
8779        // A chain link may be a plain object OR a function/class (the `router`
8780        // package sets `Router.prototype = function(){}` and hangs its methods off
8781        // that function, so the methods live in the fn-prop side table).
8782        match h.get(&p) {
8783            Some(JsObj::Object(props)) => {
8784                if let Some(v) = props.get(key) {
8785                    return Some(v.clone());
8786                }
8787            }
8788            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8789                if let Some(v) = h.fn_prop(&p, key) {
8790                    return Some(v);
8791                }
8792            }
8793            _ => {}
8794        }
8795        cur = h.proto_of(&p);
8796    }
8797    None
8798}
8799
8800/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8801pub fn lookup_accessor(
8802    h: &JsHost,
8803    recv: &Value,
8804    key: &str,
8805) -> Option<(Option<Value>, Option<Value>)> {
8806    if let Some(a) = h.own_accessor(recv, key) {
8807        return Some(a);
8808    }
8809    let mut cur = h.proto_of(recv);
8810    while let Some(p) = cur {
8811        if let Some(a) = h.own_accessor(&p, key) {
8812            return Some(a);
8813        }
8814        cur = h.proto_of(&p);
8815    }
8816    // A STATIC accessor declared by an ancestor class. A subclass reaches its
8817    // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8818    // above reads — classes are not linked there, so that walk ended at once.
8819    // Static methods and fields already inherited because `class_static` does
8820    // this same parent walk for `fn_prop`; only accessors had no equivalent:
8821    //
8822    //     class Base { static get kind() { return 'base' } }
8823    //     class Sub extends Base {}
8824    //     Sub.plain()  // worked, a fn_prop
8825    //     Sub.kind     // undefined; node reads 'base'
8826    //
8827    // The caller invokes the getter with the class it was READ off as `this`,
8828    // so a getter reading `this.x` sees the subclass, per 10.2.4.
8829    let mut cls = recv.clone();
8830    while let Some(JsObj::Class(c)) = h.get(&cls) {
8831        let Some(parent) = c.parent.clone() else {
8832            break;
8833        };
8834        if let Some(a) = h.own_accessor(&parent, key) {
8835            return Some(a);
8836        }
8837        cls = parent;
8838    }
8839    None
8840}
8841
8842/// Register a builtin error prototype (for `instanceof Error` etc.).
8843pub fn set_error_proto(name: &str, proto: Value) {
8844    with_host(|h| {
8845        h.error_protos.insert(name.to_string(), proto);
8846    });
8847}
8848pub fn error_proto(name: &str) -> Option<Value> {
8849    with_host(|h| h.error_protos.get(name).cloned())
8850}
8851/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8852pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8853    h.error_protos.get(name).cloned()
8854}
8855
8856impl JsHost {
8857    /// `Error.prototype.toString` for an object whose prototype chain reaches
8858    /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8859    /// `None` for anything that is not an error, so the caller keeps its own
8860    /// stringification.
8861    pub fn error_to_string(&self, v: &Value) -> Option<String> {
8862        let base = self.error_protos.get("Error")?;
8863        let mut cur = self.proto_of(v);
8864        let mut is_error = false;
8865        while let Some(p) = cur {
8866            if self.strict_eq(&p, base) {
8867                is_error = true;
8868                break;
8869            }
8870            cur = self.proto_of(&p);
8871        }
8872        if !is_error {
8873            return None;
8874        }
8875        let name = lookup_chain(self, v, "name")
8876            .map(|n| self.str_of(&n))
8877            .unwrap_or_else(|| "Error".into());
8878        let message = lookup_chain(self, v, "message")
8879            .map(|m| self.str_of(&m))
8880            .unwrap_or_default();
8881        // Node's internal coded errors override `toString` as
8882        // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8883        // `@@nodeError` tag marks the errors `synth_error` built from a
8884        // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8885        // property still stringifies plainly.
8886        if let Some(JsObj::Object(p)) = self.get(v) {
8887            if p.contains_key("@@nodeError") {
8888                if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8889                    return Some(format!("{name} [{code}]: {message}"));
8890                }
8891            }
8892        }
8893        Some(match (name.is_empty(), message.is_empty()) {
8894            (true, _) => message,
8895            (false, true) => name,
8896            (false, false) => format!("{name}: {message}"),
8897        })
8898    }
8899}
8900
8901/// The set of builtin error constructor names forming the error hierarchy.
8902pub const ERROR_NAMES: &[&str] = &[
8903    "Error",
8904    "TypeError",
8905    "RangeError",
8906    "SyntaxError",
8907    "ReferenceError",
8908    "EvalError",
8909    "URIError",
8910    "AggregateError",
8911    // `assert`'s error class. It is NOT a global (node exposes it only as
8912    // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8913    // has to be a name `synth_error` recognizes: without it the head
8914    // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8915    // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8916    // was `Error` and `e.message` carried a prefix node keeps out of it.
8917    "AssertionError",
8918    // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8919    // `name` comes from the SECOND constructor argument rather than from the
8920    // class, so its prototype keeps the base default and each instance stamps
8921    // its own name into an internal slot.
8922    "DOMException",
8923];
8924
8925impl JsHost {
8926    /// Lazily build the builtin error prototype chain: `Error.prototype →
8927    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8928    /// Populated once; instances link to these so `e instanceof TypeError` and
8929    /// `e instanceof Error` both hold.
8930    /// The real `Buffer.prototype` object, building the
8931    /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8932    /// first use.
8933    ///
8934    /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8935    /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8936    /// `instanceof` had to be special-cased around it. Each prototype is a
8937    /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8938    /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8939    /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8940    pub fn ensure_native_protos(&mut self) {
8941        // The wrapper prototypes share this registry and this guard would skip
8942        // them, so they are built through their own.
8943        self.ensure_wrapper_protos();
8944        self.ensure_function_kind_protos();
8945        if self.native_protos.contains_key("Buffer") {
8946            return;
8947        }
8948        let obj_proto = self.object_proto();
8949        // `Object.prototype` is the one builtin prototype that already existed as
8950        // a real object (it is the chain root). Register it so `Object.prototype`
8951        // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8952        // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8953        // compares a real object against a thunk and reads false.
8954        self.native_protos
8955            .insert("Object".to_string(), obj_proto.clone());
8956        for m in crate::builtins::OBJECT_PROTO_METHODS {
8957            let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8958            if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8959                p.insert((*m).to_string(), thunk);
8960            }
8961            self.hide_prop(&obj_proto, m);
8962        }
8963        // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8964        // Object.prototype`, which is the chain node v26.7.0 really has. The
8965        // shared iteration methods (`every`, `map`, `filter`, …) live on the
8966        // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8967        // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8968        // Node while the intermediate owns it. `%TypedArray%` is not a global,
8969        // so it is reachable only by walking the chain — exactly as in Node.
8970        // Every element kind gets its own prototype hanging off the shared
8971        // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8972        // `Int32Array.prototype` rather than some other kind's. Linking them all
8973        // to `Uint8Array.prototype` would have been the easy version and would
8974        // have made an `Int32Array` claim the wrong prototype.
8975        let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8976        for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8977            chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8978        }
8979        // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8980        chain.push(("Buffer", Value::Undef));
8981        let mut prev: Option<Value> = None;
8982        for (ctor, parent) in chain.drain(..) {
8983            let proto = self.new_object(IndexMap::new());
8984            // Each kind hangs off the shared intermediate; `Buffer` hangs off
8985            // `Uint8Array.prototype`; the intermediate itself off
8986            // `Object.prototype`.
8987            let parent = match ctor {
8988                "TypedArray" => parent,
8989                "Buffer" => self
8990                    .native_protos
8991                    .get("Uint8Array")
8992                    .cloned()
8993                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8994                _ => self
8995                    .native_protos
8996                    .get("TypedArray")
8997                    .cloned()
8998                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8999            };
9000            self.set_proto(&proto, parent);
9001            // `%TypedArray%.prototype` has no reachable constructor global, so
9002            // it gets no `constructor` slot (Node's is the anonymous
9003            // `%TypedArray%` intrinsic).
9004            if ctor != "TypedArray" {
9005                let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9006                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9007                    p.insert("constructor".into(), ctor_val);
9008                }
9009                self.hide_prop(&proto, "constructor");
9010            }
9011            let methods: &[&str] = match ctor {
9012                "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
9013                "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
9014                // `Uint8Array` alone owns the base64/hex pair — no other view
9015                // has them, which is the whole reason they cannot live on the
9016                // shared `%TypedArray%` prototype above.
9017                "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
9018                // Every other kind's prototype owns no methods; it inherits them
9019                // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
9020                // which is per-kind and which Node really keeps there (measured:
9021                // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
9022                _ => &[],
9023            };
9024            if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
9025                let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
9026                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9027                    p.insert("BYTES_PER_ELEMENT".into(), bpe);
9028                }
9029                self.hide_prop(&proto, "BYTES_PER_ELEMENT");
9030            }
9031            for m in methods {
9032                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9033                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9034                    p.insert((*m).to_string(), thunk);
9035                }
9036                self.hide_prop(&proto, m);
9037            }
9038            self.native_protos.insert(ctor.to_string(), proto.clone());
9039            prev = Some(proto);
9040        }
9041    }
9042
9043    /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
9044    /// objects.
9045    ///
9046    /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
9047    /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
9048    /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
9049    /// String.prototype` and `w instanceof String` both read false while the
9050    /// wrapper's methods still resolved through the string funnel. Registering
9051    /// them here puts them on the same footing as `Buffer.prototype`.
9052    /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
9053    /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
9054    /// function's `[[Prototype]]` really points at.
9055    ///
9056    /// None are globals (node exposes them only through
9057    /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
9058    /// rather than among the wrapper constructors. Each hangs off
9059    /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
9060    pub fn ensure_function_kind_protos(&mut self) {
9061        if self.native_protos.contains_key("GeneratorFunction") {
9062            return;
9063        }
9064        let base = self
9065            .native_protos
9066            .get("Function")
9067            .cloned()
9068            .unwrap_or_else(|| self.object_proto());
9069        for ctor in [
9070            "GeneratorFunction",
9071            "AsyncFunction",
9072            "AsyncGeneratorFunction",
9073        ] {
9074            let proto = self.new_object(IndexMap::new());
9075            self.set_proto(&proto, base.clone());
9076            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9077            let tag = self.new_str(ctor);
9078            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9079                p.insert("constructor".into(), ctor_val);
9080                p.insert("@@toStringTag".into(), tag);
9081            }
9082            self.hide_prop(&proto, "constructor");
9083            self.hide_prop(&proto, "@@toStringTag");
9084            self.native_protos.insert(ctor.to_string(), proto);
9085        }
9086    }
9087
9088    pub fn ensure_wrapper_protos(&mut self) {
9089        if self.native_protos.contains_key("String") {
9090            return;
9091        }
9092        let obj_proto = self.object_proto();
9093        for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9094            let proto = self.new_object(IndexMap::new());
9095            self.set_proto(&proto, obj_proto.clone());
9096            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9097            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9098                p.insert("constructor".into(), ctor_val);
9099            }
9100            self.hide_prop(&proto, "constructor");
9101            // The three conversions must be here because `Object.prototype`
9102            // also defines them: without a shadowing entry a wrapper would
9103            // inherit the object forms and `String(new String("a"))` would
9104            // report `[object Object]`.
9105            //
9106            // The REST are here because the prototype is a real object a script
9107            // can read a method OFF of. Only the three were installed, on the
9108            // reasoning that `charAt`/`toFixed`/… reach the primitive through
9109            // `call_method` anyway — true for `s.charAt(0)` and false for the
9110            // generic-borrowing form: `String.prototype.trim` read `undefined`,
9111            // so `String.prototype.trim.call(s)` — and `Number.prototype
9112            // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9113            // wrapper method — threw. `Array.prototype`/`Object.prototype`
9114            // already carried their whole method set; these three did not.
9115            let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9116                .into_iter()
9117                .chain(match ctor {
9118                    "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9119                    "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9120                    _ => [].iter().copied(),
9121                })
9122                // The symbol-keyed methods come from the generated intrinsic
9123                // table, so this object advertises exactly the symbol methods
9124                // node defines on it — `String.prototype[Symbol.iterator]` was
9125                // `undefined` because only the string-keyed lists were walked.
9126                .chain(crate::builtins::proto_symbol_methods(ctor))
9127                .collect();
9128            let mut seen: Vec<&str> = Vec::new();
9129            for m in methods {
9130                if seen.contains(&m) {
9131                    continue;
9132                }
9133                seen.push(m);
9134                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9135                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9136                    p.insert(m.to_string(), thunk);
9137                }
9138                self.hide_prop(&proto, m);
9139            }
9140            // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9141            // prototypes that carry a `@@toStringTag`; the other three are
9142            // branded by their internal slot instead, and node reports
9143            // `undefined` for their tag. Without it
9144            // `Object.prototype.toString.call(Symbol.prototype)` read
9145            // `[object Object]` where node says `[object Symbol]`.
9146            if matches!(ctor, "Symbol" | "BigInt") {
9147                let tag = self.new_str(ctor.to_string());
9148                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9149                    p.insert("@@toStringTag".into(), tag);
9150                }
9151                // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9152                // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9153                // writable.
9154                self.set_prop_attrs(
9155                    &proto,
9156                    "@@toStringTag",
9157                    PropAttrs {
9158                        writable: false,
9159                        enumerable: false,
9160                        configurable: true,
9161                    },
9162                );
9163            }
9164            // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9165            // numeric conversion of a symbol reaches FIRST. Its absence was
9166            // observable in the failure wording: `String(Symbol.prototype)`
9167            // throws in node because `@@toPrimitive` rejects a non-Symbol
9168            // `this`, and here the conversion fell through to `toString` and
9169            // named that method in the message instead.
9170            if ctor == "Symbol" {
9171                let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9172                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9173                    p.insert("@@toPrimitive".into(), thunk);
9174                }
9175                self.hide_prop(&proto, "@@toPrimitive");
9176            }
9177            self.native_protos.insert(ctor.to_string(), proto);
9178        }
9179    }
9180
9181    /// The cached template object for one tagged-template site, if it has been
9182    /// evaluated before.
9183    pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9184        self.template_objects.get(&key).cloned()
9185    }
9186    /// Record the template object for one tagged-template site.
9187    pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9188        self.template_objects.insert(key, v);
9189    }
9190
9191    /// The real prototype object for a builtin exotic, if it has one.
9192    pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9193        self.native_protos.get(ctor).cloned()
9194    }
9195
9196    /// The constructor name whose `.prototype` object IS `v`, for a prototype
9197    /// this host built as a real object (`String.prototype`, `TypeError
9198    /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9199    ///
9200    /// A prototype is an ORDINARY object: it carries no instance's internal
9201    /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9202    /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9203    /// before, so the brand fell through to the "does it look like an Error"
9204    /// test and answered for the prototype as if it were an instance.
9205    pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9206        if !matches!(v, Value::Obj(_)) {
9207            return None;
9208        }
9209        self.native_protos
9210            .iter()
9211            .chain(self.error_protos.iter())
9212            .find(|(_, p)| *p == v)
9213            .map(|(name, _)| name.as_str())
9214    }
9215
9216    /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9217    /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9218    ///
9219    /// `Ctor.prototype` used to read `undefined` for every native class outside the
9220    /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9221    /// that libraries still use. `iconv-lite`'s internal codec — reached from
9222    /// `raw-body` on every `express.json()` request — does exactly this:
9223    ///
9224    /// ```text
9225    /// var StringDecoder = require('string_decoder').StringDecoder;
9226    /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9227    /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9228    /// InternalDecoder.prototype = StringDecoder.prototype;
9229    /// ```
9230    ///
9231    /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9232    ///
9233    /// Methods come from `stdlib::instance_method_lists`, the same table a method
9234    /// READ consults, so the prototype can never advertise a name the dispatcher
9235    /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9236    /// dispatches against its invoke-time `this`, so a subclass instance whose
9237    /// prototype IS this object gets the native implementation. Returns `None` for
9238    /// a tag with no instance methods, leaving those constructors as they were.
9239    pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9240        if let Some(p) = self.native_protos.get(ctor) {
9241            return Some(p.clone());
9242        }
9243        // `Buffer` and the typed-array kinds belong to the chain
9244        // `ensure_native_protos` builds. Building one of them here first hung it
9245        // straight off `Object.prototype` AND registered it, which made that
9246        // chain's own `contains_key("Buffer")` guard skip the build for the rest
9247        // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9248        // Uint8Array` read false.
9249        if ctor == "Buffer"
9250            || ctor == "TypedArray"
9251            || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9252        {
9253            self.ensure_native_protos();
9254            return self.native_protos.get(ctor).cloned();
9255        }
9256        let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9257        // A class can carry accessors and no methods at all
9258        // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9259        // `asymmetricKeyDetails`), so an empty method list does not mean there
9260        // is no prototype to build.
9261        let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9262        if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9263            return None;
9264        }
9265        // A native class with a real PARENT hangs off that parent's prototype
9266        // rather than straight off `Object.prototype`. The stream hierarchy is
9267        // `Readable → Stream → EventEmitter`, which is what makes
9268        // `new Readable() instanceof Stream` hold and what an ES5 subclass
9269        // doing `Object.create(Stream.prototype)` inherits from.
9270        let parent_proto = match crate::stdlib::native_parent(ctor) {
9271            Some(p) => self
9272                .ensure_ctor_proto(p)
9273                .unwrap_or_else(|| self.object_proto()),
9274            None => self.object_proto(),
9275        };
9276        let proto = self.new_object(IndexMap::new());
9277        self.set_proto(&proto, parent_proto);
9278        let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9279        if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9280            p.insert("constructor".into(), ctor_val);
9281        }
9282        self.hide_prop(&proto, "constructor");
9283        // A prototype member is ENUMERABLE in node for every class but the few
9284        // written as ES classes, so `for (const k in url)` walks `href` and the
9285        // rest. Hiding all of them made that loop find nothing.
9286        let visible = crate::stdlib::instance_members_enumerable(ctor);
9287        let symbols = crate::builtins::proto_symbol_methods(ctor);
9288        for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9289            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9290            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9291                p.insert((*m).to_string(), thunk);
9292            }
9293            // A symbol-keyed member never enumerates.
9294            if !visible || m.starts_with("@@") {
9295                self.hide_prop(&proto, m);
9296            }
9297        }
9298        // Accessors and the class's `Symbol.toStringTag`, both of which live on
9299        // the PROTOTYPE in node — an instance owns neither.
9300        let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9301        for (key, settable) in accessors {
9302            let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9303            let set =
9304                settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9305            self.set_accessor(&proto, key, Some(get), set);
9306            if !visible {
9307                self.hide_prop(&proto, key);
9308            }
9309        }
9310        for m in crate::stdlib::instance_late_methods(ctor) {
9311            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9312            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9313                p.insert((*m).to_string(), thunk);
9314            }
9315            if !visible {
9316                self.hide_prop(&proto, m);
9317            }
9318        }
9319        if !tag.is_empty() {
9320            let tag = self.new_str(tag.to_string());
9321            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9322                p.insert("@@toStringTag".into(), tag);
9323            }
9324            self.hide_prop(&proto, "@@toStringTag");
9325        }
9326        self.native_protos.insert(ctor.to_string(), proto.clone());
9327        Some(proto)
9328    }
9329
9330    /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9331    /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9332    /// which answers from `native_protos` — does not find them.
9333    pub fn error_proto(&self, name: &str) -> Option<Value> {
9334        self.error_protos.get(name).cloned()
9335    }
9336
9337    pub fn ensure_error_protos(&mut self) {
9338        if !self.error_protos.is_empty() {
9339            return;
9340        }
9341        let obj_proto = self.object_proto();
9342        // Error.prototype first (the shared base).
9343        let err_proto = self.new_object(IndexMap::new());
9344        self.set_proto(&err_proto, obj_proto);
9345        let nm = self.new_str("Error");
9346        let empty = self.new_str("");
9347        let ctor = self.alloc(JsObj::Builtin("Error".into()));
9348        // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9349        // not a fallback the stringifier applies when nothing else matches: it
9350        // exists precisely to shadow `Object.prototype.toString`. Without it,
9351        // the first read of `Error.prototype` or `Object.prototype` — which
9352        // `x instanceof Error` performs, so ordinary code triggers it —
9353        // materialised `Object.prototype.toString`, the chain lookup started
9354        // finding it, and `String(err)` flipped from `Error: m` to
9355        // `[object Error]` for the REST OF THE PROCESS, including errors
9356        // created before the read.
9357        let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9358        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9359            p.insert("name".into(), nm);
9360            p.insert("message".into(), empty);
9361            p.insert("constructor".into(), ctor);
9362            p.insert("toString".into(), to_string);
9363        }
9364        // Everything on `Error.prototype` is non-enumerable in V8.
9365        for k in ["name", "message", "constructor", "toString"] {
9366            self.hide_prop(&err_proto, k);
9367        }
9368        self.error_protos.insert("Error".into(), err_proto.clone());
9369        for name in &ERROR_NAMES[1..] {
9370            let p = self.new_object(IndexMap::new());
9371            self.set_proto(&p, err_proto.clone());
9372            let nm = self.new_str(*name);
9373            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9374            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9375                o.insert("name".into(), nm);
9376                o.insert("constructor".into(), ctor);
9377            }
9378            self.hide_prop(&p, "name");
9379            self.hide_prop(&p, "constructor");
9380            self.error_protos.insert((*name).to_string(), p);
9381        }
9382    }
9383}
9384
9385// ── Map/Set element access (used by builtins) ────────────────────────────────
9386
9387impl JsHost {
9388    /// A function's `.length`: the count of leading params before the first one
9389    /// with a default or the rest element.
9390    pub fn func_arity(&self, v: &Value) -> usize {
9391        // 20.2.3.2: a bound function's `length` is the target's, less the
9392        // arguments already bound, floored at 0. Reporting 0 for every bound
9393        // function breaks arity dispatch — express picks error-handling
9394        // middleware with `fn.length === 4`, so a bound handler was never
9395        // recognised as one.
9396        if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9397            return self.func_arity(&target.clone()).saturating_sub(args.len());
9398        }
9399        // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9400        // reports 1 rather than the 0 a target of unknown arity would give.
9401        if let Some(JsObj::Builtin(n)) = self.get(v) {
9402            return crate::builtins::builtin_meta(n)
9403                .map(|(_, len)| len as usize)
9404                .unwrap_or(0);
9405        }
9406        let def_id = match self.get(v) {
9407            Some(JsObj::Func(f)) => Some(f.def_id),
9408            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9409                Some(JsObj::Func(f)) => Some(f.def_id),
9410                _ => None,
9411            },
9412            _ => None,
9413        };
9414        match def_id.and_then(|id| self.funcs.get(id)) {
9415            Some(def) => def
9416                .params
9417                .iter()
9418                .take_while(|p| !p.rest && !p.has_default)
9419                .count(),
9420            None => 0,
9421        }
9422    }
9423
9424    pub fn is_map(&self, v: &Value) -> bool {
9425        matches!(self.get(v), Some(JsObj::Map { .. }))
9426    }
9427    pub fn is_set(&self, v: &Value) -> bool {
9428        matches!(self.get(v), Some(JsObj::Set { .. }))
9429    }
9430}
9431
9432// ── promises & the event loop ────────────────────────────────────────────────
9433
9434impl JsHost {
9435    /// Allocate a fresh pending promise, returning its heap value.
9436    pub fn new_promise(&mut self) -> Value {
9437        let id = self.promises.len() as u32;
9438        self.promises.push(PromiseCell {
9439            state: PromiseState::Pending,
9440            value: Value::Undef,
9441            reactions: Vec::new(),
9442            handled: false,
9443        });
9444        self.alloc(JsObj::Promise { id })
9445    }
9446    pub fn promise_id(&self, v: &Value) -> Option<u32> {
9447        match self.get(v) {
9448            Some(JsObj::Promise { id }) => Some(*id),
9449            _ => None,
9450        }
9451    }
9452    pub fn promise_state(&self, id: u32) -> PromiseState {
9453        self.promises[id as usize].state
9454    }
9455    pub fn promise_value(&self, id: u32) -> Value {
9456        self.promises[id as usize].value.clone()
9457    }
9458    pub fn promise_mark_handled(&mut self, id: u32) {
9459        self.promises[id as usize].handled = true;
9460    }
9461    /// Take the pending reactions of a promise (called on settle).
9462    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9463        std::mem::take(&mut self.promises[id as usize].reactions)
9464    }
9465    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9466        self.promises[id as usize].reactions.push(r);
9467    }
9468    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9469        let c = &mut self.promises[id as usize];
9470        if c.state != PromiseState::Pending {
9471            return; // already settled — resolve/reject are one-shot
9472        }
9473        c.state = state;
9474        c.value = value;
9475    }
9476    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9477        self.microtasks.push_back(Task::Js { cb, args });
9478    }
9479    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9480        self.nextticks.push_back(Task::Js { cb, args });
9481    }
9482    /// Schedule a native (Rust) microtask — used by Promise reactions and async
9483    /// resumption.
9484    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9485        self.microtasks.push_back(Task::Native(f));
9486    }
9487    /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9488    /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9489    /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9490    /// `ref`/`unref` and `refresh` can find this entry again.
9491    pub fn add_timer(
9492        &mut self,
9493        delay: f64,
9494        callback: Value,
9495        args: Vec<Value>,
9496        interval: Option<f64>,
9497    ) -> u64 {
9498        let id = self.next_timer;
9499        self.next_timer += 1;
9500        // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9501        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9502        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9503        self.macrotasks.push(Timer {
9504            id,
9505            delay,
9506            seq: id,
9507            callback,
9508            args,
9509            cancelled: false,
9510            interval,
9511            refed: true,
9512            deadline,
9513        });
9514        id
9515    }
9516    /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9517    /// `clearInterval` from *inside* the callback cancels this very entry) and
9518    /// taking a fresh `seq` so same-delay peers still round-robin.
9519    ///
9520    /// Called BEFORE the callback runs: if it were called after, the entry would
9521    /// be absent while the callback executed and a `clearInterval(t)` there would
9522    /// cancel nothing, resurrecting an interval the program had stopped.
9523    fn rearm_timer(&mut self, t: &Timer, period: f64) {
9524        let seq = self.next_timer;
9525        self.next_timer += 1;
9526        let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9527        self.macrotasks.push(Timer {
9528            id: t.id,
9529            delay: t.delay,
9530            seq,
9531            callback: t.callback.clone(),
9532            args: t.args.clone(),
9533            cancelled: false,
9534            interval: Some(period),
9535            refed: t.refed,
9536            deadline,
9537        });
9538    }
9539    /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9540    /// timer. A no-op once the timer has fired or been cleared (Node likewise
9541    /// treats `ref`/`unref` on a dead timer as inert).
9542    pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9543        for t in &mut self.macrotasks {
9544            if t.id == id && !t.cancelled {
9545                t.refed = refed;
9546            }
9547        }
9548    }
9549    /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9550    /// A fired or cleared timer reports `false`, matching Node.
9551    pub fn timer_has_ref(&self, id: u64) -> bool {
9552        self.macrotasks
9553            .iter()
9554            .any(|t| t.id == id && !t.cancelled && t.refed)
9555    }
9556    /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9557    /// just been scheduled.
9558    pub fn refresh_timer(&mut self, id: u64) {
9559        let now = Instant::now();
9560        for t in &mut self.macrotasks {
9561            if t.id == id && !t.cancelled {
9562                t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9563            }
9564        }
9565    }
9566    /// Clone the I/O sender for a background I/O thread.
9567    pub fn io_sender(&self) -> Sender<IoTask> {
9568        self.io_tx.clone()
9569    }
9570    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9571    /// alive.
9572    pub fn incr_handle(&mut self) {
9573        self.open_handles += 1;
9574    }
9575    /// Release a handle; the loop exits once this reaches `0` with empty queues.
9576    pub fn decr_handle(&mut self) {
9577        self.open_handles = self.open_handles.saturating_sub(1);
9578    }
9579    pub fn open_handles(&self) -> usize {
9580        self.open_handles
9581    }
9582    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9583    /// path). Ties break by `seq`.
9584    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9585        let idx = self
9586            .macrotasks
9587            .iter()
9588            .enumerate()
9589            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9590            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9591            .map(|(i, _)| i);
9592        idx.map(|i| self.macrotasks.remove(i))
9593    }
9594    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9595    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9596    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9597        self.macrotasks
9598            .iter()
9599            .filter(|t| !t.cancelled)
9600            .map(|t| t.deadline)
9601            .min()
9602            .map(|d| d.saturating_duration_since(now))
9603    }
9604    pub fn cancel_timer(&mut self, id: u64) {
9605        for t in &mut self.macrotasks {
9606            if t.id == id {
9607                t.cancelled = true;
9608            }
9609        }
9610    }
9611    fn pop_next_timer(&mut self) -> Option<Timer> {
9612        // Earliest (delay, seq) fires first — a deterministic virtual clock.
9613        let idx = self
9614            .macrotasks
9615            .iter()
9616            .enumerate()
9617            .filter(|(_, t)| !t.cancelled)
9618            .min_by(|(_, a), (_, b)| {
9619                a.delay
9620                    .partial_cmp(&b.delay)
9621                    .unwrap_or(std::cmp::Ordering::Equal)
9622                    .then(a.seq.cmp(&b.seq))
9623            })
9624            .map(|(i, _)| i);
9625        idx.map(|i| self.macrotasks.remove(i))
9626    }
9627    fn next_microtask(&mut self) -> Option<Task> {
9628        // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9629        // queue, then drain the microtask queue in full, then repeat if the
9630        // microtasks queued more ticks. A tick queued from INSIDE a microtask
9631        // therefore waits for the rest of that microtask queue.
9632        //
9633        // Preferring ticks on every step interleaved the two, so
9634        // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9635        // promise callbacks queued behind it — the one ordering difference a
9636        // library scheduling work from a `.then` can actually observe.
9637        if !self.draining_micro {
9638            if let Some(t) = self.nextticks.pop_front() {
9639                return Some(t);
9640            }
9641        }
9642        if let Some(t) = self.microtasks.pop_front() {
9643            // Stay in the microtask phase until this queue is exhausted.
9644            self.draining_micro = !self.microtasks.is_empty();
9645            return Some(t);
9646        }
9647        self.draining_micro = false;
9648        self.nextticks.pop_front()
9649    }
9650    fn has_microtasks(&self) -> bool {
9651        !self.nextticks.is_empty() || !self.microtasks.is_empty()
9652    }
9653    /// Whether any pending timer is *referenced* — the timer half of Node's
9654    /// handle count. Only these keep the loop alive; unref'd timers still fire
9655    /// while something else holds the loop open, but never hold it themselves.
9656    fn has_refed_macrotasks(&self) -> bool {
9657        self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9658    }
9659    /// Whether any pending timer repeats. A repeating timer cannot run on the
9660    /// virtual clock: virtual time never advances, so the interval would re-arm
9661    /// at the same instant forever, spinning a core and starving every
9662    /// longer-delay timer behind it. Its presence forces the real clock.
9663    fn has_pending_interval(&self) -> bool {
9664        self.macrotasks
9665            .iter()
9666            .any(|t| !t.cancelled && t.interval.is_some())
9667    }
9668}
9669
9670/// Drive the event loop to quiescence.
9671///
9672/// **Liveness** is Node's handle count: the loop runs while a microtask is
9673/// pending, an open handle is registered (a listening server, a live socket, an
9674/// in-flight async op), or a *referenced* timer is still pending. That last term
9675/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9676/// does in Node — the interval re-arms itself, so a ref'd timer is always
9677/// pending and the loop never reaches its exit condition.
9678///
9679/// Two **clock regimes**, selected per iteration:
9680///
9681/// - **Virtual clock** (no open handles and no repeating timer): the original
9682///   deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9683///   real waiting. Parity output and test speed for ordinary `setTimeout`
9684///   scripts are unchanged.
9685/// - **Real clock** (an open handle, or any pending interval): fire every timer
9686///   whose wall-clock deadline has passed, then BLOCK on the I/O channel
9687///   (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9688///   timers) and run the received `IoTask` on the main thread. The host keeps
9689///   its own `Sender`, so `recv` never disconnects while the process should stay
9690///   alive.
9691///
9692///   A repeating timer *must* take this path: virtual time never advances, so an
9693///   interval on the virtual clock would re-fire at the same instant forever,
9694///   spinning a core and starving every longer-delay timer behind it.
9695///
9696/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9697pub fn run_event_loop() -> Result<(), String> {
9698    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9699    // host borrow); restore it afterward so a re-entrant run reuses the channel.
9700    let rx = with_host(|h| h.io_rx.take());
9701    let result = drive_event_loop(rx.as_ref());
9702    with_host(|h| h.io_rx = rx);
9703    result
9704}
9705
9706fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9707    loop {
9708        // 1) Exhaust the microtask queue (nextTick before promise reactions),
9709        //    then report anything that rejected with nobody watching.
9710        while let Some(task) = with_host(|h| h.next_microtask()) {
9711            task.run()?;
9712        }
9713        check_unhandled_rejections()?;
9714
9715        // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9716        //    process exits, dropping any unref'd timers still pending — which is
9717        //    why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9718        //    timer behind a ref'd one does.
9719        let alive =
9720            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9721        if !alive {
9722            break;
9723        }
9724
9725        // 3) Pick the clock regime for this turn.
9726        let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9727        if virtual_clock {
9728            // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9729            match with_host(|h| h.pop_next_timer()) {
9730                Some(t) => fire_timer(t)?,
9731                // Unreachable while `alive` holds (a ref'd timer must exist),
9732                // but exiting is the safe reading of "nothing left to run".
9733                None => break,
9734            }
9735            continue;
9736        }
9737
9738        // ── real-clock / blocking-I/O regime ─────────────────────────────────
9739        let now = Instant::now();
9740        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9741            fire_timer(t)?;
9742            continue; // re-drain microtasks, re-check deadlines
9743        }
9744        // Nothing due and no pending microtasks: block for the next I/O event,
9745        // bounded by the soonest timer deadline so due timers still fire on time.
9746        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9747        let timeout = with_host(|h| h.next_timer_timeout(now));
9748        let recv = match timeout {
9749            Some(d) => rx.recv_timeout(d),
9750            None => rx
9751                .recv()
9752                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9753        };
9754        match recv {
9755            Ok(task) => task()?,
9756            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9757            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9758        }
9759    }
9760    Ok(())
9761}
9762
9763/// Run one due timer's callback, first re-arming it if it repeats.
9764///
9765/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9766/// issued from inside that callback cancels the next occurrence. Re-arming
9767/// afterwards would leave the interval absent from the queue for the duration of
9768/// its own callback, so the `clear` would match nothing and the freshly pushed
9769/// entry would resurrect an interval the program had just stopped.
9770fn fire_timer(t: Timer) -> Result<(), String> {
9771    if let Some(period) = t.interval {
9772        with_host(|h| h.rearm_timer(&t, period));
9773    }
9774    invoke(&t.callback, t.args, None)?;
9775    Ok(())
9776}
9777
9778// ── async functions & promise resolution (native) ────────────────────────────
9779
9780/// Drive a freshly-built async coroutine and return its result promise.
9781fn run_async(gen: Value) -> Value {
9782    let result = with_host(|h| h.new_promise());
9783    let rid = with_host(|h| h.promise_id(&result).unwrap());
9784    drive_async(gen, rid, Value::Undef);
9785    result
9786}
9787
9788/// Resume an async coroutine one step, wiring `await` continuations to promise
9789/// settlement.
9790fn drive_async(gen: Value, rid: u32, send: Value) {
9791    match gen_resume(&gen, send) {
9792        Ok(GenStep::Yield(awaited)) => {
9793            let ap = promise_of(&awaited);
9794            let aid = with_host(|h| h.promise_id(&ap).unwrap());
9795            let gen2 = gen.clone();
9796            subscribe_native(
9797                aid,
9798                Box::new(move |state, val| {
9799                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
9800                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9801                    let tag = if state == PromiseState::Rejected {
9802                        1.0
9803                    } else {
9804                        0.0
9805                    };
9806                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9807                    drive_async(gen2, rid, packet);
9808                    Ok(())
9809                }),
9810            );
9811        }
9812        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9813        Err(e) => {
9814            let ev = take_exc_or_error(&e);
9815            reject_promise_val(rid, ev);
9816        }
9817    }
9818}
9819
9820/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9821/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9822pub fn await_value(awaited: Value) -> Result<Value, String> {
9823    // Inside an `async function*`, `await` and `yield` share one coroutine
9824    // yielder, so an awaited value has to be tagged or the driver would hand it
9825    // to the consumer as if the body had yielded it.
9826    let awaited = match CUR_GEN.with(|c| c.get()) {
9827        Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9828            let mut m = IndexMap::new();
9829            m.insert(AWAIT_MARKER.to_string(), awaited);
9830            h.new_object(m)
9831        }),
9832        _ => awaited,
9833    };
9834    let packet = gen_yield(awaited)?;
9835    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9836    let tag = items
9837        .first()
9838        .map(|v| with_host(|h| h.to_number(v)))
9839        .unwrap_or(0.0);
9840    let val = items.get(1).cloned().unwrap_or(Value::Undef);
9841    if tag == 1.0 {
9842        with_host(|h| h.exc = Some(val.clone()));
9843        Err(with_host(|h| crate::builtins::error_string(h, &val)))
9844    } else {
9845        Ok(val)
9846    }
9847}
9848
9849/// Hidden key marking an `await` suspension inside an async generator.
9850const AWAIT_MARKER: &str = "@@await";
9851
9852/// The operand of an `await` suspension, or `None` for a real `yield`.
9853fn await_marker(v: &Value) -> Option<Value> {
9854    with_host(|h| match h.get(v) {
9855        Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9856        _ => None,
9857    })
9858}
9859
9860/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9861/// hand back the promise its `{value, done}` record (or rejection) will settle.
9862///
9863/// All three of `.next`, `.return` and `.throw` come through here, so a request
9864/// never resumes the body while an earlier one is still suspended on an
9865/// internal `await`.
9866pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9867    let step = with_host(|h| h.new_promise());
9868    let sid = with_host(|h| h.promise_id(&step).unwrap());
9869    let id = match with_host(|h| match h.get(gen) {
9870        Some(JsObj::Generator { id }) => Some(*id),
9871        _ => None,
9872    }) {
9873        Some(id) => id,
9874        None => return step,
9875    };
9876    with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9877    pump_async_gen(gen.clone(), id);
9878    step
9879}
9880
9881/// One `.next(v)` of an `async function*`.
9882pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9883    async_gen_enqueue(gen, GenReq::Next(send))
9884}
9885
9886/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9887/// already in flight (the body may only be resumed by one request at a time).
9888fn pump_async_gen(gen: Value, id: u32) {
9889    if with_host(|h| h.generators[id as usize].running) {
9890        return;
9891    }
9892    let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9893        return;
9894    };
9895    with_host(|h| h.generators[id as usize].running = true);
9896    start_async_gen_req(gen, sid, req);
9897}
9898
9899/// Begin one queued request: resume the body with the completion it carries,
9900/// then hand the outcome to the shared continuation.
9901///
9902/// A RETURN completion always Awaits its value before the body sees it — via
9903/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9904/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9905/// when it is not yet started or already completed. So a `.return()` settles one
9906/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9907/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9908/// `.return()` overtake the reactions of the `.next()` it followed.
9909fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9910    if matches!(req, GenReq::Return(_)) {
9911        with_host(|h| {
9912            h.queue_micro_native(Box::new(move || {
9913                resume_async_gen_req(gen, sid, req);
9914                Ok(())
9915            }))
9916        });
9917        return;
9918    }
9919    resume_async_gen_req(gen, sid, req);
9920}
9921
9922/// Deliver a queued completion to the body and settle its step promise.
9923fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9924    let step = match req {
9925        GenReq::Next(v) => gen_resume(&gen, v),
9926        GenReq::Return(v) => gen_return(&gen, v),
9927        GenReq::Throw(e) => gen_throw(&gen, e),
9928    };
9929    settle_async_gen_step(gen, sid, step);
9930}
9931
9932/// One request has settled: release the body and start the next queued request.
9933fn finish_async_gen_step(gen: Value, id: u32) {
9934    with_host(|h| h.generators[id as usize].running = false);
9935    pump_async_gen(gen, id);
9936}
9937
9938/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9939pub fn is_async_generator(v: &Value) -> bool {
9940    let id = match with_host(|h| match h.get(v) {
9941        Some(JsObj::Generator { id }) => Some(*id),
9942        _ => None,
9943    }) {
9944        Some(id) => id,
9945        None => return false,
9946    };
9947    with_host(|h| h.generators[id as usize].async_gen)
9948}
9949
9950/// A `{ value, done }` iterator-result object.
9951fn iter_record(value: Value, done: bool) -> Value {
9952    with_host(|h| {
9953        let mut m = IndexMap::new();
9954        m.insert("value".to_string(), value);
9955        m.insert("done".to_string(), Value::Bool(done));
9956        h.new_object(m)
9957    })
9958}
9959
9960/// Resume a request that was suspended on an internal `await` (always a normal
9961/// completion — the awaited promise's outcome rides in `packet`).
9962fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9963    let step = gen_resume(&gen, packet);
9964    settle_async_gen_step(gen, sid, step);
9965}
9966
9967/// Turn one body resumption into a settled step promise: transparently re-drive
9968/// internal `await` suspensions, and settle on the first REAL yield or on the
9969/// body's completion. Shared by the initial resume of a queued request and by
9970/// every await-resumption of it.
9971fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9972    let id = match with_host(|h| match h.get(&gen) {
9973        Some(JsObj::Generator { id }) => Some(*id),
9974        _ => None,
9975    }) {
9976        Some(id) => id,
9977        None => return,
9978    };
9979    match step {
9980        Ok(GenStep::Yield(v)) => match await_marker(&v) {
9981            Some(awaited) => {
9982                // An internal `await`: settle it, then resume the body. The
9983                // request stays in flight across the suspension.
9984                let ap = promise_of(&awaited);
9985                let aid = with_host(|h| h.promise_id(&ap).unwrap());
9986                subscribe_native(
9987                    aid,
9988                    Box::new(move |state, val| {
9989                        let tag = if state == PromiseState::Rejected {
9990                            1.0
9991                        } else {
9992                            0.0
9993                        };
9994                        let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9995                        drive_async_gen(gen.clone(), sid, packet);
9996                        Ok(())
9997                    }),
9998                );
9999            }
10000            // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
10001            // AWAITED before the step promise settles, so `yield somePromise`
10002            // hands the consumer the RESOLVED value (and costs its microtask).
10003            None => {
10004                let yp = promise_of(&v);
10005                let yid = with_host(|h| h.promise_id(&yp).unwrap());
10006                subscribe_native(
10007                    yid,
10008                    Box::new(move |state, val| {
10009                        if state == PromiseState::Rejected {
10010                            reject_promise_val(sid, val);
10011                        } else {
10012                            resolve_promise_val(sid, iter_record(val, false));
10013                        }
10014                        finish_async_gen_step(gen.clone(), id);
10015                        Ok(())
10016                    }),
10017                );
10018            }
10019        },
10020        Ok(GenStep::Done(v)) => {
10021            resolve_promise_val(sid, iter_record(v, true));
10022            finish_async_gen_step(gen, id);
10023        }
10024        Err(e) => {
10025            let ev = take_exc_or_error(&e);
10026            reject_promise_val(sid, ev);
10027            finish_async_gen_step(gen, id);
10028        }
10029    }
10030}
10031
10032/// A promise for `v`: `v` itself if it is already a promise, else a promise
10033/// resolved with `v`.
10034pub fn promise_of(v: &Value) -> Value {
10035    if with_host(|h| h.promise_id(v)).is_some() {
10036        return v.clone();
10037    }
10038    let p = with_host(|h| h.new_promise());
10039    let id = with_host(|h| h.promise_id(&p).unwrap());
10040    resolve_promise_val(id, v.clone());
10041    p
10042}
10043
10044/// Register a native reaction on promise `id` (schedules immediately if already
10045/// settled).
10046pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
10047    // A native continuation (`await`, promise adoption, `for await`) observes a
10048    // rejection exactly as a `.catch` does, so it is not "unhandled".
10049    with_host(|h| h.promise_mark_handled(id));
10050    let state = with_host(|h| h.promise_state(id));
10051    if state == PromiseState::Pending {
10052        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
10053    } else {
10054        let val = with_host(|h| h.promise_value(id));
10055        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
10056    }
10057}
10058
10059/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
10060/// else fulfill with it.
10061pub fn resolve_promise_val(id: u32, value: Value) {
10062    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10063        return;
10064    }
10065    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10066        if vid == id {
10067            // Resolving a promise with itself → reject with a TypeError.
10068            let e = with_host(|h| {
10069                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10070            });
10071            reject_promise_val(id, e);
10072            return;
10073        }
10074        // A native promise is still a thenable, so the spec routes it through
10075        // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10076        // is even registered. (`await` does NOT pay this: V8's await optimization
10077        // subscribes to a native promise directly, which `await_value` mirrors.)
10078        with_host(|h| {
10079            h.queue_micro_native(Box::new(move || {
10080                subscribe_native(
10081                    vid,
10082                    Box::new(move |state, val| {
10083                        with_host(|h| h.settle_promise(id, state, val.clone()));
10084                        schedule_reactions(id);
10085                        Ok(())
10086                    }),
10087                );
10088                Ok(())
10089            }))
10090        });
10091        return;
10092    }
10093    // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10094    // through a dedicated job — the promise adopts what `then` reports, it is
10095    // never fulfilled WITH the thenable itself.
10096    if let Some(then) = thenable_then(&value) {
10097        with_host(|h| {
10098            h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10099        });
10100        return;
10101    }
10102    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10103    schedule_reactions(id);
10104}
10105
10106/// `value.then` if `value` is an object with a callable `then` — the test that
10107/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10108fn thenable_then(value: &Value) -> Option<Value> {
10109    // A PROXY is not a plain object and supplies `then` through its `get` trap,
10110    // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10111    // WITH the proxy instead of adopting it.
10112    if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10113        return protocol_lookup(value, "then")
10114            .ok()
10115            .flatten()
10116            .filter(|f| with_host(|h| is_callable(h, f)));
10117    }
10118    if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10119        return None;
10120    }
10121    let then = with_host(|h| lookup_chain(h, value, "then"))?;
10122    with_host(|h| is_callable(h, &then)).then_some(then)
10123}
10124
10125/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10126/// reject continuations and let it settle us. A throw out of `then` rejects.
10127fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10128    let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10129    let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10130    if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10131        let ev = take_exc_or_error(&e);
10132        reject_promise_val(id, ev);
10133    }
10134    Ok(())
10135}
10136
10137pub fn reject_promise_val(id: u32, value: Value) {
10138    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10139        return;
10140    }
10141    with_host(|h| {
10142        h.settle_promise(id, PromiseState::Rejected, value);
10143        h.pending_rejections.push(id);
10144    });
10145    schedule_reactions(id);
10146}
10147
10148/// Report every promise that settled rejected since the last checkpoint and
10149/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10150/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10151/// `process.on('unhandledRejection')` listener takes it.
10152fn check_unhandled_rejections() -> Result<(), String> {
10153    loop {
10154        let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10155        if ids.is_empty() {
10156            return Ok(());
10157        }
10158        for id in ids {
10159            let unhandled = with_host(|h| {
10160                h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10161            });
10162            if !unhandled {
10163                continue;
10164            }
10165            // Report each promise at most once, however many checkpoints pass.
10166            with_host(|h| h.promise_mark_handled(id));
10167            let val = with_host(|h| h.promise_value(id));
10168            let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10169            if listeners.is_empty() {
10170                let msg = with_host(|h| crate::builtins::error_string(h, &val));
10171                with_host(|h| h.exc = Some(val));
10172                return Err(msg);
10173            }
10174            let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10175            for f in listeners {
10176                invoke(&f, vec![val.clone(), promise.clone()], None)?;
10177            }
10178        }
10179    }
10180}
10181
10182/// Drain a settled promise's reactions into microtasks.
10183fn schedule_reactions(id: u32) {
10184    let reactions = with_host(|h| h.take_reactions(id));
10185    let state = with_host(|h| h.promise_state(id));
10186    let value = with_host(|h| h.promise_value(id));
10187    for r in reactions {
10188        let value = value.clone();
10189        match r {
10190            PromiseReaction::Native(f) => {
10191                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10192            }
10193            PromiseReaction::Js {
10194                on_ful,
10195                on_rej,
10196                result,
10197            } => {
10198                with_host(|h| {
10199                    h.queue_micro_native(Box::new(move || {
10200                        run_js_reaction(state, value, on_ful, on_rej, result)
10201                    }))
10202                });
10203            }
10204        }
10205    }
10206}
10207
10208/// Run a `.then` reaction: call the appropriate handler and settle the result
10209/// promise with its outcome (or pass through if there is no handler).
10210fn run_js_reaction(
10211    state: PromiseState,
10212    value: Value,
10213    on_ful: Value,
10214    on_rej: Value,
10215    result: Value,
10216) -> Result<(), String> {
10217    let rid = match with_host(|h| h.promise_id(&result)) {
10218        Some(i) => i,
10219        None => return Ok(()),
10220    };
10221    let handler = if state == PromiseState::Rejected {
10222        on_rej
10223    } else {
10224        on_ful
10225    };
10226    if with_host(|h| is_callable(h, &handler)) {
10227        match invoke(&handler, vec![value], None) {
10228            Ok(r) => resolve_promise_val(rid, r),
10229            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10230        }
10231    } else if state == PromiseState::Rejected {
10232        reject_promise_val(rid, value);
10233    } else {
10234        resolve_promise_val(rid, value);
10235    }
10236    Ok(())
10237}
10238
10239/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10240/// synthesized `Error` from the internal message.
10241pub fn take_exc_or_error(e: &str) -> Value {
10242    with_host(|h| {
10243        h.error.take();
10244        h.exc
10245            .take()
10246            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10247    })
10248}
10249
10250/// Register a user `.then` reaction (JS handlers + result promise).
10251pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10252    let id = match with_host(|h| h.promise_id(p)) {
10253        Some(i) => i,
10254        None => return Value::Undef,
10255    };
10256    with_host(|h| h.promise_mark_handled(id));
10257    // The result is built with the RECEIVER's species, so a subclass promise
10258    // stays a subclass promise through a `.then` chain.
10259    let result = match crate::builtins::promise_species_from(p) {
10260        Ok(Some(sp)) => sp,
10261        _ => with_host(|h| h.new_promise()),
10262    };
10263    let reaction = PromiseReaction::Js {
10264        on_ful,
10265        on_rej,
10266        result: result.clone(),
10267    };
10268    let state = with_host(|h| h.promise_state(id));
10269    if state == PromiseState::Pending {
10270        with_host(|h| h.add_reaction(id, reaction));
10271    } else {
10272        let value = with_host(|h| h.promise_value(id));
10273        if let PromiseReaction::Js {
10274            on_ful,
10275            on_rej,
10276            result,
10277        } = reaction
10278        {
10279            with_host(|h| {
10280                h.queue_micro_native(Box::new(move || {
10281                    run_js_reaction(state, value, on_ful, on_rej, result)
10282                }))
10283            });
10284        }
10285    }
10286    result
10287}
10288
10289/// The ReferenceError for touching `this` in a derived constructor before
10290/// `super()` — or returning from one without calling it.
10291pub fn this_before_super_error() -> String {
10292    "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10293}
10294
10295/// The intrinsic a builtin name denotes. Two property paths that the spec
10296/// defines as the SAME function object compare `===`: `Number.parseInt` is
10297/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10298/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10299fn builtin_identity(name: &str) -> &str {
10300    match name {
10301        "Number.parseInt" => "parseInt",
10302        "Number.parseFloat" => "parseFloat",
10303        _ => name,
10304    }
10305}