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            // `callable_name`, not the class record's own name: an anonymous
1385            // class expression is named by inference from its binding
1386            // (`const X = class {}`), which lands as an own `name` property,
1387            // and node prints `new X()` as `X {}`.
1388            if let Some(JsObj::Class(_)) = self.get(&c) {
1389                return self.callable_name(&c);
1390            }
1391        }
1392        // A `function F(){}` constructor is not a `class`, so it has no
1393        // `proto_class` entry. V8's `getConstructorName` walks the prototype
1394        // chain for an own `constructor` that is a named function — which is
1395        // what makes `console.log(new F())` print `F { y: 2 }`.
1396        let mut cur = self.proto_of(obj);
1397        while let Some(p) = cur {
1398            let ctor = match self.get(&p) {
1399                Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1400                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1401                _ => None,
1402            };
1403            if let Some(f) = ctor {
1404                let n = self.callable_name(&f);
1405                if !n.is_empty() {
1406                    return n;
1407                }
1408            }
1409            cur = self.proto_of(&p);
1410        }
1411        String::new()
1412    }
1413
1414    /// The prefix node's `getPrefix` gives a builtin collection: `Map(2) ` for
1415    /// a plain one, `M2(2) [Map] ` for an instance of a subclass (constructor
1416    /// name, then the builtin tag in brackets). `size` is `None` for a kind
1417    /// with no count (`Promise`).
1418    fn builtin_prefix(&self, v: &Value, tag: &str, size: Option<usize>) -> String {
1419        let ctor = self.ctor_name(v);
1420        let count = size.map(|n| format!("({n})")).unwrap_or_default();
1421        if ctor.is_empty() || ctor == tag {
1422            format!("{tag}{count} ")
1423        } else {
1424            format!("{ctor}{count} [{tag}] ")
1425        }
1426    }
1427
1428    /// The `[Name]` stub a builtin collection collapses to past the depth
1429    /// limit: `[Map]`, or `[M2 [Map]]` for a subclass instance.
1430    fn builtin_depth_stub(&self, v: &Value, tag: &str) -> String {
1431        let ctor = self.ctor_name(v);
1432        if ctor.is_empty() || ctor == tag {
1433            format!("[{tag}]")
1434        } else {
1435            format!("[{ctor} [{tag}]]")
1436        }
1437    }
1438
1439    /// Whether a callable owns a `prototype` property. `MakeConstructor`
1440    /// (10.2.5) runs for an ordinary function definition and for every
1441    /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1442    /// function are not constructors and own none.
1443    pub fn owns_prototype(&self, v: &Value) -> bool {
1444        match self.get(v) {
1445            Some(JsObj::Class(_)) => true,
1446            Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1447                Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1448                None => false,
1449            },
1450            _ => false,
1451        }
1452    }
1453
1454    /// A function's own-property table (created on demand).
1455    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1456        if let Value::Obj(i) = v {
1457            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1458        } else {
1459            None
1460        }
1461    }
1462
1463    /// A class static member, inherited down the constructor chain: a subclass
1464    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1465    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1466        let mut cur = class_val.clone();
1467        loop {
1468            if let Some(v) = self.fn_prop(&cur, name) {
1469                return Some(v);
1470            }
1471            match self.get(&cur) {
1472                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1473                _ => return None,
1474            }
1475        }
1476    }
1477
1478    /// The first `extends` ancestor that is NOT a user class — the builtin
1479    /// constructor a class chain bottoms out in (`class D extends Array {}` →
1480    /// the `Array` builtin), or `None` for a chain of user classes only.
1481    ///
1482    /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1483    /// stops being a `Class`, so a static declared by the BUILTIN half of the
1484    /// chain was unreachable: `D.from` read `undefined` where node inherits
1485    /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1486    /// with an ordinary property read, which is what reaches a builtin's
1487    /// statics.
1488    pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1489        let mut cur = class_val.clone();
1490        loop {
1491            match self.get(&cur) {
1492                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1493                _ => return Some(cur),
1494            }
1495        }
1496    }
1497    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1498        if let Value::Obj(i) = v {
1499            self.fn_props
1500                .entry(*i)
1501                .or_default()
1502                .insert(name.to_string(), val);
1503        }
1504        // `name` and `prototype` are own properties of every function/class, but
1505        // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1506        // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1507        // script assigned. An ARRAY receiver reaching the same side table has no
1508        // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1509        if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1510            return;
1511        }
1512        let attrs = match name {
1513            "name" => PropAttrs {
1514                writable: false,
1515                enumerable: false,
1516                configurable: true,
1517            },
1518            "prototype" => PropAttrs {
1519                writable: true,
1520                enumerable: false,
1521                configurable: false,
1522            },
1523            _ => return,
1524        };
1525        self.set_prop_attrs(v, name, attrs);
1526    }
1527    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1528    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1529        self.builtin_statics
1530            .get(ns)
1531            .and_then(|m| m.get(name).cloned())
1532    }
1533    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1534    /// handles for the same namespace).
1535    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1536        self.builtin_statics
1537            .entry(ns.to_string())
1538            .or_default()
1539            .insert(name.to_string(), val);
1540    }
1541    /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1542    /// key was there — without this, `delete Array.prototype.patch` answered
1543    /// true and left the entry in place, so the patch outlived its own removal.
1544    pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1545        self.builtin_statics
1546            .get_mut(ns)
1547            .is_some_and(|m| m.shift_remove(name).is_some())
1548    }
1549    /// Every namespace a script has assigned a static onto, with that
1550    /// namespace's assigned keys — the source of the user-added half of
1551    /// `Object.getOwnPropertyNames(Array.prototype)`.
1552    pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1553        self.builtin_statics
1554            .get(ns)
1555            .map(|m| m.keys().cloned().collect())
1556            .unwrap_or_default()
1557    }
1558    /// Drop an own property from the side table (`delete arr.foo`,
1559    /// `delete fn.tag`). Reports whether the key was there.
1560    pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1561        match v {
1562            Value::Obj(i) => self
1563                .fn_props
1564                .get_mut(i)
1565                .map(|m| m.shift_remove(name).is_some())
1566                .unwrap_or(false),
1567            _ => false,
1568        }
1569    }
1570    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1571        if let Value::Obj(i) = v {
1572            self.fn_props
1573                .get(i)
1574                .map(|m| m.keys().cloned().collect())
1575                .unwrap_or_default()
1576        } else {
1577            Vec::new()
1578        }
1579    }
1580
1581    /// Install an accessor `(get, set)` for `key` on the object `owner`.
1582    pub fn set_accessor(
1583        &mut self,
1584        owner: &Value,
1585        key: &str,
1586        get: Option<Value>,
1587        set: Option<Value>,
1588    ) {
1589        if let Value::Obj(i) = owner {
1590            // Accessors live in their own table, but JS reports own keys in a
1591            // single insertion order across data AND accessor properties. Drop an
1592            // ordering marker into the property map so
1593            // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1594            // The marker is `@@`-prefixed, so it is invisible to every reader.
1595            let marker = format!("{ORD_MARKER}{key}");
1596            match self.get_mut(owner) {
1597                Some(JsObj::Object(props)) => {
1598                    if !props.contains_key(key) && !props.contains_key(&marker) {
1599                        props.insert(marker, Value::Undef);
1600                    }
1601                }
1602                // A function or class keeps its own properties in the fn-prop
1603                // side table, so its ordering marker belongs there. Without it a
1604                // static accessor enumerated AFTER every static field and method
1605                // regardless of where the class body declared it: node reports
1606                // `class A { static s = 2; static get sv(){} static m(){} }` as
1607                // `['sv', 'm', 's']` — the methods and accessors in source order
1608                // first, then the fields — and this reported `['m', 's', 'sv']`.
1609                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1610                    let table = self.fn_props.entry(*i).or_default();
1611                    if !table.contains_key(key) && !table.contains_key(&marker) {
1612                        table.insert(marker, Value::Undef);
1613                    }
1614                }
1615                _ => {}
1616            }
1617            let slot = self
1618                .accessors
1619                .entry(*i)
1620                .or_default()
1621                .entry(key.to_string())
1622                .or_insert((None, None));
1623            if get.is_some() {
1624                slot.0 = get;
1625            }
1626            if set.is_some() {
1627                slot.1 = set;
1628            }
1629        }
1630    }
1631    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1632    /// Drop an own accessor property entirely, marker and all.
1633    ///
1634    /// `delete obj.accessorProp` used to clear only the property map, and an
1635    /// accessor does not live there — so the delete reported success while the
1636    /// getter kept answering and `in` kept reporting the key.
1637    pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1638        if let Value::Obj(i) = owner {
1639            if let Some(m) = self.accessors.get_mut(i) {
1640                m.shift_remove(key);
1641            }
1642        }
1643        let marker = format!("{ORD_MARKER}{key}");
1644        match self.get_mut(owner) {
1645            Some(JsObj::Object(props)) => {
1646                props.shift_remove(&marker);
1647            }
1648            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1649                if let Value::Obj(i) = owner {
1650                    if let Some(t) = self.fn_props.get_mut(i) {
1651                        t.shift_remove(&marker);
1652                    }
1653                }
1654            }
1655            _ => {}
1656        }
1657    }
1658
1659    /// Turn an own accessor property into a data property carrying `value`,
1660    /// keeping its place in the own-key order.
1661    ///
1662    /// `set_accessor` records that order with an `@@ord:` marker in the
1663    /// property map rather than a real key, so deleting the accessor and
1664    /// inserting the value would append the key at the end instead. Node
1665    /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1666    /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1667    pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1668        if let Value::Obj(i) = owner {
1669            if let Some(m) = self.accessors.get_mut(i) {
1670                m.shift_remove(key);
1671            }
1672        }
1673        let marker = format!("{ORD_MARKER}{key}");
1674        let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1675            Some(pos) => {
1676                *map = map
1677                    .iter()
1678                    .enumerate()
1679                    .map(|(n, (k, v))| {
1680                        if n == pos {
1681                            (key.to_string(), value.clone())
1682                        } else {
1683                            (k.clone(), v.clone())
1684                        }
1685                    })
1686                    .collect();
1687            }
1688            None => {
1689                map.insert(key.to_string(), value.clone());
1690            }
1691        };
1692        let fn_table = matches!(
1693            self.get(owner),
1694            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1695        );
1696        if fn_table {
1697            if let Value::Obj(i) = owner {
1698                swap(self.fn_props.entry(*i).or_default());
1699            }
1700        } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1701            swap(props);
1702        }
1703    }
1704
1705    /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1706    ///
1707    /// Used when one object becomes another in place (a class extending a
1708    /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1709    /// already points at the leaf class's prototype, which is the one its
1710    /// methods must resolve through.
1711    pub fn move_index_state(&mut self, src: u32, dst: u32) {
1712        if let Some(holes) = self.array_holes.remove(&src) {
1713            self.array_holes.insert(dst, holes);
1714        }
1715        if let Some(attrs) = self.prop_attrs.remove(&src) {
1716            self.prop_attrs.entry(dst).or_default().extend(attrs);
1717        }
1718        if let Some(props) = self.fn_props.remove(&src) {
1719            self.fn_props.entry(dst).or_default().extend(props);
1720        }
1721        if let Some(acc) = self.accessors.remove(&src) {
1722            self.accessors.entry(dst).or_default().extend(acc);
1723        }
1724    }
1725
1726    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1727        if let Value::Obj(i) = owner {
1728            self.accessors.get(i).and_then(|m| m.get(key).cloned())
1729        } else {
1730            None
1731        }
1732    }
1733
1734    /// The own accessor-property keys of `owner`, in installation order.
1735    pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1736        match owner {
1737            Value::Obj(i) => self
1738                .accessors
1739                .get(i)
1740                .map(|m| m.keys().cloned().collect())
1741                .unwrap_or_default(),
1742            _ => Vec::new(),
1743        }
1744    }
1745
1746    // ── own-property attributes ──────────────────────────────────────────
1747
1748    /// Record non-default attributes for `owner[key]`. Storing the default shape
1749    /// clears the entry so the table only ever holds deviations.
1750    pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1751        if let Value::Obj(i) = owner {
1752            if attrs == PropAttrs::default() {
1753                if let Some(m) = self.prop_attrs.get_mut(i) {
1754                    m.shift_remove(key);
1755                }
1756            } else {
1757                self.prop_attrs
1758                    .entry(*i)
1759                    .or_default()
1760                    .insert(key.to_string(), attrs);
1761            }
1762        }
1763    }
1764
1765    /// Copy every recorded property attribute from `from` to `to`. A pass that
1766    /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1767    /// across or the copy silently re-exposes non-enumerable slots.
1768    pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1769        if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1770            if let Some(m) = self.prop_attrs.get(f).cloned() {
1771                for (k, a) in m {
1772                    self.set_prop_attrs(to, &k, a);
1773                }
1774            }
1775        }
1776    }
1777
1778    /// The attributes of own property `owner[key]` (all-true when unrecorded).
1779    pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1780        // An array's `length` is the array exotic's own property (10.4.2):
1781        // never enumerated and never configurable, and writable until
1782        // `Object.freeze` clears that — which is what stops a `push` from
1783        // extending a frozen array. Reporting it unconditionally writable made
1784        // `Object.isFrozen(Object.freeze([]))` false once the elements started
1785        // being sealed, because `length` was then the one key that never
1786        // followed.
1787        if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1788            let writable = match owner {
1789                Value::Obj(i) => self
1790                    .prop_attrs
1791                    .get(i)
1792                    .and_then(|m| m.get(key))
1793                    .map(|a| a.writable)
1794                    .unwrap_or(true),
1795                _ => true,
1796            };
1797            return PropAttrs {
1798                writable,
1799                enumerable: false,
1800                // An ARGUMENTS object's `length` is an ordinary data property
1801                // (10.4.4.6), so it is configurable where a real array's is
1802                // not. The two share a backing representation here, so the
1803                // exotic's attributes have to be told apart explicitly.
1804                configurable: crate::builtins::is_arguments_h(self, owner),
1805            };
1806        }
1807        match owner {
1808            Value::Obj(i) => self
1809                .prop_attrs
1810                .get(i)
1811                .and_then(|m| m.get(key))
1812                .copied()
1813                .unwrap_or_default(),
1814            _ => PropAttrs::default(),
1815        }
1816    }
1817
1818    /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1819    /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1820    pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1821        !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1822    }
1823
1824    /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1825    /// shape of every V8 "hidden but real" own property.
1826    pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1827        self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1828    }
1829
1830    /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1831    /// non-writable data property silently ignores the write in sloppy mode,
1832    /// which is the mode every script here runs in; so does adding a *new* key to
1833    /// a non-extensible object.
1834    pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1835        if !self.prop_attrs(owner, key).writable {
1836            return false;
1837        }
1838        // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1839        // and those members own no map entry for the walk below to find:
1840        // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1841        // is refused in node and was creating an own property here, which then
1842        // changed the object's brand.
1843        // The receiver's OWN kind counts too, not only the prototypes an
1844        // explicit link reaches: a plain array inherits `Array.prototype`
1845        // implicitly, with no link for the walk to follow, and
1846        // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1847        //
1848        // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1849        // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1850        // — are also OWN properties of every instance, so the inherited rule
1851        // never decides them; applying it anyway blocked `SetFunctionName`
1852        // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1853        // (x) {…}})` then threw.
1854        if key.starts_with("@@")
1855            && crate::builtins::own_ctor_name(self, owner)
1856                .into_iter()
1857                .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1858                .any(|c| crate::builtins::is_proto_readonly(c, key))
1859        {
1860            return false;
1861        }
1862        // 10.1.9.2: with no OWN property, the inherited one decides. A
1863        // non-writable data property up the chain blocks the write rather than
1864        // being shadowed — including one on a frozen prototype. Only own
1865        // attributes were consulted, so `Object.create(frozenBase).f = 2`
1866        // quietly created an own property node refuses to create.
1867        //
1868        // An inherited ACCESSOR does not block: its setter runs, and the write
1869        // path checks for one before reaching here.
1870        let has_own = match self.get(owner) {
1871            Some(JsObj::Object(p)) => p.contains_key(key),
1872            _ => true,
1873        };
1874        if !has_own {
1875            let mut cur = self.proto_of(owner);
1876            while let Some(proto) = cur {
1877                if self.own_accessor(&proto, key).is_some() {
1878                    break;
1879                }
1880                let present =
1881                    matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1882                if present {
1883                    if !self.prop_attrs(&proto, key).writable {
1884                        return false;
1885                    }
1886                    break;
1887                }
1888                cur = self.proto_of(&proto);
1889            }
1890        }
1891        if self.is_extensible(owner) {
1892            return true;
1893        }
1894        // A non-extensible object refuses a NEW key. Only the plain-object arm
1895        // could name its own keys, so every other shape answered "own" for any
1896        // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1897        // to the frozen template object a tagged template hands its tag.
1898        match self.get(owner) {
1899            Some(JsObj::Object(p)) => p.contains_key(key),
1900            Some(JsObj::Array(items)) => {
1901                key == "length"
1902                    || key
1903                        .parse::<usize>()
1904                        .map(|i| i < items.len())
1905                        .unwrap_or(false)
1906                    || self.fn_prop(owner, key).is_some()
1907            }
1908            // A RegExp's `lastIndex` is an own property, so a merely
1909            // NON-EXTENSIBLE regexp still accepts a write to it.
1910            Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1911            // Every other shape keeps its own properties in the fn-prop side
1912            // table (a function's statics, a Map's assigned properties), so
1913            // "does it already own this key" is that table's question. Answering
1914            // a blanket `true` let a NEW key land on a frozen function and a
1915            // frozen Map.
1916            _ => self.fn_prop(owner, key).is_some(),
1917        }
1918    }
1919
1920    /// Mark `v` closed to new properties (`Object.preventExtensions`).
1921    pub fn prevent_extensions(&mut self, v: &Value) {
1922        if let Value::Obj(i) = v {
1923            self.non_extensible.insert(*i);
1924        }
1925    }
1926
1927    pub fn is_extensible(&self, v: &Value) -> bool {
1928        !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1929    }
1930
1931    /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1932    /// the object and strip `configurable` — and, when freezing, `writable` —
1933    /// from every own property, data and accessor alike.
1934    pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1935        self.prevent_extensions(v);
1936        let mut keys = self.integrity_keys(v);
1937        keys.extend(self.own_accessor_keys(v));
1938        for k in keys {
1939            let mut a = self.prop_attrs(v, &k);
1940            a.configurable = false;
1941            if freeze {
1942                a.writable = false;
1943            }
1944            self.set_prop_attrs(v, &k, a);
1945        }
1946    }
1947
1948    /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1949    ///
1950    /// An array's elements are own properties too, and only the `Object` arm was
1951    /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1952    /// through, and the elements still reported `writable: true,
1953    /// configurable: true` while `Object.isFrozen` answered true over an empty
1954    /// key list. `length` is an own property as well, and freezing it is what
1955    /// stops a `push` from extending a frozen array.
1956    fn integrity_keys(&self, v: &Value) -> Vec<String> {
1957        let side_table_keys = |v: &Value| -> Vec<String> {
1958            match v {
1959                Value::Obj(i) => self
1960                    .fn_props
1961                    .get(i)
1962                    .map(|m| m.keys().cloned().collect())
1963                    .unwrap_or_default(),
1964                _ => Vec::new(),
1965            }
1966        };
1967        match self.get(v) {
1968            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1969            // A RegExp's only own property is its `lastIndex` cursor, which
1970            // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1971            // sealed nothing and a frozen regexp's cursor still moved.
1972            Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1973            // A function's statics and a Map's assigned properties live in the
1974            // fn-prop side table, and freezing has to reach them too.
1975            Some(JsObj::Func(_))
1976            | Some(JsObj::Class(_))
1977            | Some(JsObj::Map { .. })
1978            | Some(JsObj::Set { .. })
1979            | Some(JsObj::Promise { .. }) => side_table_keys(v),
1980            Some(JsObj::Array(items)) => (0..items.len())
1981                .map(|i| i.to_string())
1982                .chain(std::iter::once("length".to_string()))
1983                // A named property stuck on an array (`a.tag = 't'`, a match
1984                // array's `.index`/`.groups`) is an own property too, and
1985                // freezing has to reach it.
1986                .chain(match v {
1987                    Value::Obj(i) => self
1988                        .fn_props
1989                        .get(i)
1990                        .map(|m| m.keys().cloned().collect::<Vec<_>>())
1991                        .unwrap_or_default(),
1992                    _ => Vec::new(),
1993                })
1994                .collect(),
1995            _ => Vec::new(),
1996        }
1997    }
1998
1999    /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
2000    pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
2001        if self.is_extensible(v) {
2002            return false;
2003        }
2004        let mut keys = self.integrity_keys(v);
2005        keys.extend(self.own_accessor_keys(v));
2006        keys.iter().all(|k| {
2007            let a = self.prop_attrs(v, k);
2008            !a.configurable && (!freeze || !a.writable)
2009        })
2010    }
2011
2012    /// A fresh unique `Symbol(desc)` value.
2013    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
2014        let id = self.next_symbol;
2015        self.next_symbol += 1;
2016        let v = self.alloc(JsObj::Symbol { desc, id });
2017        self.symbols_by_id.insert(id, v.clone());
2018        v
2019    }
2020
2021    /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
2022    /// well-known `@@iterator`) came from.
2023    pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
2024        if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
2025            return self.symbols_by_id.get(&id).cloned();
2026        }
2027        let name = k.strip_prefix("@@")?;
2028        WELL_KNOWN_SYMBOLS
2029            .contains(&name)
2030            .then(|| {
2031                self.symbol_registry
2032                    .get(&format!("@@Symbol.{name}"))
2033                    .cloned()
2034            })
2035            .flatten()
2036    }
2037
2038    /// The own symbol-keyed property keys of `v` as SYMBOL values —
2039    /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
2040    pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
2041        let keys: Vec<String> = match self.get(v) {
2042            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
2043            // An Array/Function receiver has no property map: its non-index own
2044            // properties — symbol-keyed ones included — live in the fn-prop side
2045            // table, and are just as much own properties as an object's.
2046            Some(_) => self.fn_prop_keys(v),
2047            None => return Vec::new(),
2048        };
2049        keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2050    }
2051
2052    /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2053    /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2054    /// alongside the string keys, and what `Object.keys` / `for-in` /
2055    /// `JSON.stringify` deliberately skip.
2056    pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2057        match self.get(v) {
2058            Some(JsObj::Object(p)) => p
2059                .iter()
2060                .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2061                .map(|(k, val)| (k.clone(), val.clone()))
2062                .collect(),
2063            // Array/Function: the side table (see `own_symbol_keys`).
2064            Some(_) => self
2065                .fn_prop_keys(v)
2066                .into_iter()
2067                .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2068                .map(|k| {
2069                    let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2070                    (k, val)
2071                })
2072                .collect(),
2073            None => Vec::new(),
2074        }
2075    }
2076    /// The shared `Symbol.for(key)` value (interned by description).
2077    pub fn symbol_for(&mut self, key: &str) -> Value {
2078        if let Some(v) = self.symbol_registry.get(key) {
2079            return v.clone();
2080        }
2081        let s = self.new_symbol(Some(key.to_string()));
2082        self.symbol_registry.insert(key.to_string(), s.clone());
2083        s
2084    }
2085    /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2086    /// or `undefined` for a symbol that is not in the registry at all.
2087    ///
2088    /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2089    /// `Symbol('k')` share a description and only the first is registered. The
2090    /// `@@Symbol.*` well-known entries are registry-internal and never a
2091    /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2092    /// `undefined` there.
2093    pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2094        let Some(key) = self
2095            .symbol_registry
2096            .iter()
2097            .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2098            .map(|(k, _)| k.clone())
2099        else {
2100            return Value::Undef;
2101        };
2102        self.new_str(key)
2103    }
2104    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2105    /// property key is `@@iterator`).
2106    pub fn well_known_iterator(&mut self) -> Value {
2107        self.symbol_for("@@Symbol.iterator")
2108    }
2109    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2110    pub fn well_known_async_iterator(&mut self) -> Value {
2111        self.symbol_for("@@Symbol.asyncIterator")
2112    }
2113    /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2114    /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2115    /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2116    ///
2117    /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2118    /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2119    /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2120    /// different symbol, and identification is by id, so a user-made
2121    /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2122    pub fn well_known_symbol(&mut self, name: &str) -> Value {
2123        let key = format!("@@Symbol.{name}");
2124        if let Some(v) = self.symbol_registry.get(&key) {
2125            return v.clone();
2126        }
2127        let s = self.new_symbol(Some(format!("Symbol.{name}")));
2128        if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2129            self.well_known_ids.insert(*id, name.to_string());
2130        }
2131        self.symbol_registry.insert(key, s.clone());
2132        s
2133    }
2134    /// The internal property-key string for a value used as a key. A `Symbol`
2135    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2136    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2137    pub fn property_key(&self, v: &Value) -> String {
2138        if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2139            if let Some(n) = self.well_known_ids.get(id) {
2140                return format!("@@{n}");
2141            }
2142            return format!("@@sym:{id}");
2143        }
2144        self.str_of(v)
2145    }
2146
2147    pub fn null(&self) -> Value {
2148        self.null_val.clone()
2149    }
2150    pub fn is_null(&self, v: &Value) -> bool {
2151        matches!(self.get(v), Some(JsObj::Null))
2152    }
2153
2154    // ── program loading ──────────────────────────────────────────────────
2155    pub fn program_offsets(&self) -> (usize, usize) {
2156        (self.funcs.len(), self.tries.len())
2157    }
2158    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2159        self.funcs.extend(funcs);
2160        self.tries.extend(tries);
2161    }
2162    /// The source text of function `def_id`, when its program kept one.
2163    pub fn func_source(&self, def_id: usize) -> Option<&str> {
2164        let d = self.funcs.get(def_id)?;
2165        let (start, end) = d.span;
2166        if end == 0 {
2167            return None;
2168        }
2169        self.scripts
2170            .get(d.script? as usize)?
2171            .get(start as usize..end as usize)
2172    }
2173    pub fn try_def(&self, id: usize) -> Option<TryDef> {
2174        self.tries.get(id).cloned()
2175    }
2176
2177    /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2178    /// finalizer)` — without copying its chunks. Running a `try` used to clone
2179    /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2180    /// handler and its finalizer on every iteration just to learn its shape.
2181    pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2182        let t = self.tries.get(id)?;
2183        Some((
2184            t.handler.is_some(),
2185            t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2186            t.finalizer.is_some(),
2187        ))
2188    }
2189
2190    /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2191    /// Reached only when no pooled VM already holds that chunk.
2192    pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2193        let t = self.tries.get(id)?;
2194        match part {
2195            0 => Some(t.block.clone()),
2196            1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2197            _ => t.finalizer.clone(),
2198        }
2199    }
2200
2201    // ── heap allocation / accessors ──────────────────────────────────────
2202    pub fn alloc(&mut self, obj: JsObj) -> Value {
2203        self.heap.push(obj);
2204        Value::Obj((self.heap.len() - 1) as u32)
2205    }
2206    pub fn get(&self, v: &Value) -> Option<&JsObj> {
2207        if let Value::Obj(i) = v {
2208            self.heap.get(*i as usize)
2209        } else {
2210            None
2211        }
2212    }
2213    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2214        if let Value::Obj(i) = v {
2215            self.heap.get_mut(*i as usize)
2216        } else {
2217            None
2218        }
2219    }
2220    /// Which variant `v` points at, without copying its contents. Use this in
2221    /// place of `get(v).cloned()` whenever only the tag is needed — see
2222    /// [`ObjKind`].
2223    pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2224        self.get(v).map(JsObj::kind)
2225    }
2226    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2227        self.alloc(JsObj::Str(s.into()))
2228    }
2229    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2230        self.alloc(JsObj::Array(items))
2231    }
2232
2233    /// Record that `name` was declared as a private method or accessor.
2234    pub fn note_private_method(&mut self, name: &str) {
2235        self.private_methods.insert(name.to_string());
2236    }
2237
2238    /// Whether `name` was declared as a private method/accessor by some class,
2239    /// as opposed to a private field.
2240    pub fn is_private_method(&self, name: &str) -> bool {
2241        self.private_methods.contains(name)
2242    }
2243
2244    /// The name of the class whose body the running function belongs to. Only a
2245    /// method of that class can even mention its private names, so this is the
2246    /// class a failed brand check must name.
2247    /// The `super` binding of the frame now running: the owning class name,
2248    /// whether the method is static, and the home object of an object-literal
2249    /// method. An ARROW captures all three at creation, the way it captures
2250    /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2251    /// Whether the activation now running is strict code.
2252    /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2253    /// with no `'use strict'` of its own or inherited from its script. This is
2254    /// the receiver test the `arguments`/`caller` poison pill keys on: node
2255    /// decides by the FUNCTION, never by the code doing the reading.
2256    pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2257        match self.get(v) {
2258            Some(JsObj::Func(fv)) => {
2259                !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2260            }
2261            _ => false,
2262        }
2263    }
2264    pub fn current_strict(&self) -> bool {
2265        self.frame().strict
2266    }
2267
2268    /// Mark the frame about to run as STRICT — used for a program whose own top
2269    /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2270    pub fn set_current_strict(&mut self) {
2271        if let Some(f) = self.frames.last_mut() {
2272            f.strict = true;
2273        }
2274    }
2275
2276    pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2277        (
2278            self.current_home_class_name(),
2279            self.frame().home_static,
2280            self.frame().home_object.clone(),
2281        )
2282    }
2283
2284    pub fn current_home_class_name(&self) -> Option<String> {
2285        match self.get(&self.current_home_class()?) {
2286            Some(JsObj::Class(c)) => Some(c.name.clone()),
2287            _ => None,
2288        }
2289    }
2290
2291    /// Whether `recv` — or anything on its prototype chain — carries the private
2292    /// name `key`. A private FIELD is an own property of the instance; a private
2293    /// METHOD lives on the class prototype, one link up.
2294    pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2295        let mut cur = Some(recv.clone());
2296        while let Some(v) = cur {
2297            let owns = match self.get(&v) {
2298                Some(JsObj::Object(p)) => p.contains_key(key),
2299                Some(JsObj::Class(c)) => c.statics.contains_key(key),
2300                _ => false,
2301            };
2302            if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2303                return true;
2304            }
2305            cur = self.proto_of(&v);
2306        }
2307        false
2308    }
2309
2310    // ── array holes ──────────────────────────────────────────────────────
2311    //
2312    // Every read/write of an array's elision set goes through this block. See
2313    // the `array_holes` field for why the marker lives here rather than in
2314    // `Value`.
2315
2316    /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2317    /// opposed to a stored `undefined`. `false` for anything that is not an
2318    /// array, and for every index of a dense one.
2319    pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2320        match (arr, ()) {
2321            (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2322            _ => false,
2323        }
2324    }
2325
2326    /// Whether `arr` has any elided element at all — one hash probe, and the
2327    /// guard every hole-aware code path takes before doing anything slower.
2328    pub fn has_holes(&self, arr: &Value) -> bool {
2329        matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2330    }
2331
2332    /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2333    /// Sorted because every consumer (own-key enumeration, `util.inspect`
2334    /// run-grouping) needs index order, and the backing set has none.
2335    pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2336        let Value::Obj(i) = arr else {
2337            return Vec::new();
2338        };
2339        let Some(hs) = self.array_holes.get(i) else {
2340            return Vec::new();
2341        };
2342        let mut v: Vec<usize> = hs.iter().copied().collect();
2343        v.sort_unstable();
2344        v
2345    }
2346
2347    /// Record element `i` of `arr` as elided.
2348    pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2349        if let Value::Obj(idx) = arr {
2350            self.array_holes.entry(*idx).or_default().insert(i);
2351        }
2352    }
2353
2354    /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2355    /// the gap a write past the end opens).
2356    pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2357        if range.is_empty() {
2358            return;
2359        }
2360        if let Value::Obj(idx) = arr {
2361            self.array_holes.entry(*idx).or_default().extend(range);
2362        }
2363    }
2364
2365    /// Element `i` now holds a real value: it is no longer a hole. Every write
2366    /// to an array index calls this, which is what keeps a stale hole record
2367    /// from outliving the elision it described.
2368    pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2369        let Value::Obj(idx) = arr else { return };
2370        let Some(hs) = self.array_holes.get_mut(idx) else {
2371            return;
2372        };
2373        hs.remove(&i);
2374        if hs.is_empty() {
2375            self.array_holes.remove(idx);
2376        }
2377    }
2378
2379    /// `arr` is dense from here on (`fill` over the whole array, a fresh
2380    /// dense assignment into an existing handle).
2381    pub fn clear_holes(&mut self, arr: &Value) {
2382        if let Value::Obj(idx) = arr {
2383            self.array_holes.remove(idx);
2384        }
2385    }
2386
2387    /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2388    /// `f`. Used by every method that derives a new array whose holes track the
2389    /// source's (`slice`, `concat`, `map`).
2390    pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2391        if !self.has_holes(src) {
2392            return;
2393        }
2394        let moved: rustc_hash::FxHashSet<usize> =
2395            self.hole_indices(src).into_iter().filter_map(f).collect();
2396        self.install_holes(dst, moved);
2397    }
2398
2399    /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2400    /// existing hole moves to, or `None` if the mutation removed it. This is the
2401    /// one primitive behind every structural array mutation — `shift` is
2402    /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2403    /// `splice` is the general case.
2404    pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2405        if !self.has_holes(arr) {
2406            return;
2407        }
2408        let moved: rustc_hash::FxHashSet<usize> =
2409            self.hole_indices(arr).into_iter().filter_map(f).collect();
2410        self.install_holes(arr, moved);
2411    }
2412
2413    /// Replace `arr`'s elision set outright, dropping the record entirely when
2414    /// the new set is empty so `has_holes` stays a single negative probe for the
2415    /// dense case.
2416    pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2417        let Value::Obj(idx) = arr else { return };
2418        if holes.is_empty() {
2419            self.array_holes.remove(idx);
2420        } else {
2421            self.array_holes.insert(*idx, holes);
2422        }
2423    }
2424
2425    /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2426    /// truncating `splice` leaves behind.
2427    pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2428        self.remap_holes(arr, |i| (i < len).then_some(i));
2429    }
2430
2431    /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2432    /// array, where each maximal run of elided positions collapses to a single
2433    /// `<N empty items>` entry. Returns the entries and whether the last of them
2434    /// is the `... N more items` tail (which the grid layout must not size a
2435    /// column to).
2436    ///
2437    /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2438    /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2439    /// sparse arrays print a single `<200 empty items>`.
2440    fn inspect_sparse(
2441        &self,
2442        v: &Value,
2443        items: &[Value],
2444        indent: usize,
2445        st: &mut InspectCycles,
2446    ) -> (Vec<String>, bool) {
2447        let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2448        let empties = |n: usize| {
2449            let unit = if n == 1 { "item" } else { "items" };
2450            format!("<{n} empty {unit}>")
2451        };
2452        let mut out: Vec<String> = Vec::new();
2453        // The first index not yet accounted for by an entry.
2454        let mut index = 0usize;
2455        for (i, it) in items.iter().enumerate() {
2456            if out.len() >= inspect_max_array_length() {
2457                break;
2458            }
2459            if holes.contains(&i) {
2460                continue;
2461            }
2462            if i > index {
2463                out.push(empties(i - index));
2464                index = i;
2465                if out.len() >= inspect_max_array_length() {
2466                    break;
2467                }
2468            }
2469            out.push(self.inspect_lvl(it, indent + 2, st));
2470            index = i + 1;
2471        }
2472        let remaining = items.len() - index;
2473        if remaining == 0 {
2474            return (out, false);
2475        }
2476        if out.len() < inspect_max_array_length() {
2477            // Trailing holes are still `<N empty items>`, not a truncation.
2478            out.push(empties(remaining));
2479            (out, false)
2480        } else {
2481            let unit = if remaining == 1 { "item" } else { "items" };
2482            out.push(format!("... {remaining} more {unit}"));
2483            (out, true)
2484        }
2485    }
2486    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2487        // Integer-index keys enumerate ascending-first regardless of the order
2488        // they were supplied in (object literal, spread, Object.assign result).
2489        canonicalize_own_keys(&mut props);
2490        // A map carrying the hidden `@@native` tag IS an instance of that native
2491        // class, so it hangs off the class prototype rather than
2492        // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2493        // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2494        // `AbortController` among them — built plain objects instead, so
2495        // `x.constructor.name` read `"Object"` and a chain walk found none of
2496        // the class's methods. Linking HERE means a construction site cannot
2497        // forget it; the tag is already in the map at every one of them.
2498        let tag = props.get("@@native").and_then(|v| self.as_str(v));
2499        let obj = self.alloc(JsObj::Object(props));
2500        if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2501            self.set_proto(&obj, proto);
2502        }
2503        obj
2504    }
2505    pub fn as_str(&self, v: &Value) -> Option<String> {
2506        match v {
2507            Value::Str(s) => Some((**s).clone()),
2508            Value::Obj(_) => match self.get(v) {
2509                Some(JsObj::Str(s)) => Some(s.clone()),
2510                _ => None,
2511            },
2512            _ => None,
2513        }
2514    }
2515
2516    // ── scope / names ────────────────────────────────────────────────────
2517    fn frame(&self) -> &Frame {
2518        self.frames.last().unwrap()
2519    }
2520    fn cur_env(&self) -> Env {
2521        self.frame().env.clone()
2522    }
2523
2524    // ── DAP debug introspection (used only under `--dap`) ────────────────────
2525    /// Number of active call frames (the debugger's step-depth reference).
2526    pub fn frame_depth(&self) -> usize {
2527        self.frames.len()
2528    }
2529    /// Record the source line the innermost frame is executing (DAP line hook).
2530    pub fn set_cur_line(&mut self, line: u32) {
2531        if let Some(f) = self.frames.last_mut() {
2532            f.line = line;
2533        }
2534    }
2535    /// The `.stack` tail for an error created right now: one `    at <name>`
2536    /// line per live frame, innermost first, ending at the module frame.
2537    ///
2538    /// These are the REAL user frames — node-js has no `file:line:column` (the
2539    /// per-frame line is only tracked under `--dap`) and no Node-internal
2540    /// module-loader frames, so `.stack` names the call chain but can never be
2541    /// byte-identical to V8's. The names are what makes a thrown error
2542    /// diagnosable; the missing positions are documented in BUGS.md.
2543    /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2544    ///
2545    /// The default is 10, it is settable, and setting it to 0 is the documented
2546    /// way to make error construction cheap. It did not exist, so the read was
2547    /// `undefined` and every stack carried every frame regardless.
2548    pub fn stack_trace_limit(&self) -> usize {
2549        match self.builtin_static("Error", "stackTraceLimit") {
2550            Some(v) => {
2551                let n = self.to_number(&v);
2552                if n.is_finite() && n > 0.0 {
2553                    n as usize
2554                } else if n.is_nan() || n <= 0.0 {
2555                    0
2556                } else {
2557                    usize::MAX
2558                }
2559            }
2560            None => 10,
2561        }
2562    }
2563
2564    pub fn stack_frames(&self) -> String {
2565        let limit = self.stack_trace_limit();
2566        if limit == 0 {
2567            return String::new();
2568        }
2569        let mut out = String::new();
2570        for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2571            let name = match (&f.owner, i) {
2572                (Some(n), _) => n.clone(),
2573                (None, 0) => "Object.<anonymous>".to_string(),
2574                (None, _) => "<anonymous>".to_string(),
2575            };
2576            out.push_str("\n    at ");
2577            out.push_str(&name);
2578        }
2579        if out.is_empty() && limit > 0 {
2580            out.push_str("\n    at <anonymous>");
2581        }
2582        out
2583    }
2584
2585    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2586    /// `stackTrace`. `owner` carries the function name where known.
2587    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2588        self.frames
2589            .iter()
2590            .rev()
2591            .map(|f| {
2592                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2593                (name, f.line)
2594            })
2595            .collect()
2596    }
2597    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2598    pub fn dbg_locals(&self) -> Vec<(String, String)> {
2599        let env = self.cur_env();
2600        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2601        names
2602            .into_iter()
2603            .map(|n| {
2604                let v = self.read_name(&n).unwrap_or(Value::Undef);
2605                (n, self.inspect(&v))
2606            })
2607            .collect()
2608    }
2609
2610    /// Scope-chain read: local + enclosing chain, then globals.
2611    /// Whether `name` is a module-top-level binding that has not reached its
2612    /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2613    /// block-scoped one by inspecting the value it holds.
2614    pub fn is_tdz_global(&self, name: &str) -> bool {
2615        self.tdz_globals.contains(name)
2616    }
2617
2618    pub fn read_name(&self, name: &str) -> Option<Value> {
2619        let mut env = Some(self.cur_env());
2620        while let Some(e) = env {
2621            if let Some(v) = e.borrow().vars.get(name) {
2622                return Some(v.clone());
2623            }
2624            env = e.borrow().parent.clone();
2625        }
2626        self.globals.get(name).cloned()
2627    }
2628    pub fn read_global(&self, name: &str) -> Option<Value> {
2629        self.globals.get(name).cloned()
2630    }
2631
2632    /// Whether `name` is bound anywhere on the scope chain or in the globals —
2633    /// `read_name(..).is_some()` without cloning the value it finds. The
2634    /// strict-mode assignment path asks this and nothing else.
2635    pub fn has_name(&self, name: &str) -> bool {
2636        let mut env = Some(self.cur_env());
2637        while let Some(e) = env {
2638            if e.borrow().vars.contains_key(name) {
2639                return true;
2640            }
2641            env = e.borrow().parent.clone();
2642        }
2643        self.globals.contains_key(name)
2644    }
2645
2646    /// Assign to an existing binding up the scope chain, else create a global
2647    /// (JS assignment to an undeclared name targets the global object).
2648    /// Assign to an existing binding, or create a global. Returns `false` when
2649    /// the nearest binding is an immutable (`const`) one, which the caller turns
2650    /// into `TypeError: Assignment to constant variable.` — assigning to a
2651    /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2652    /// a mutated constant.
2653    #[must_use]
2654    pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2655        let mut env = Some(self.cur_env());
2656        while let Some(e) = env {
2657            // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2658            // binding hashed the name twice and allocated a fresh `String` key
2659            // for a key that was already there — once per assignment, so once
2660            // per loop iteration in any counting loop.
2661            //
2662            // The const check runs only at the env that OWNS the name, and the
2663            // `is_empty` guard settles the common (no consts here) case without
2664            // hashing the name again.
2665            let mut b = e.borrow_mut();
2666            if b.vars.contains_key(name) {
2667                if !b.consts.is_empty() && b.consts.contains(name) {
2668                    return false;
2669                }
2670                if let Some(slot) = b.vars.get_mut(name) {
2671                    *slot = val;
2672                }
2673                return true;
2674            }
2675            drop(b);
2676            env = e.borrow().parent.clone();
2677        }
2678        if self.global_consts.contains(name) {
2679            return false;
2680        }
2681        match self.globals.get_mut(name) {
2682            Some(slot) => *slot = val,
2683            None => {
2684                self.globals.insert(name.to_string(), val);
2685            }
2686        }
2687        true
2688    }
2689
2690    /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2691    /// plus recording the name as immutable in whichever scope received it.
2692    pub fn declare_const_name(&mut self, name: &str, val: Value) {
2693        let f = self.frame();
2694        let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2695        self.declare_name(name, val);
2696        if to_globals {
2697            self.global_consts.insert(name.to_string());
2698        } else {
2699            self.cur_env().borrow_mut().consts.insert(name.to_string());
2700        }
2701    }
2702
2703    /// The value a lexical binding holds between entering its scope and reaching
2704    /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2705    /// process, so the check is a heap-index comparison and the marker cannot be
2706    /// produced by any JavaScript expression. It never escapes: every path that
2707    /// could read it throws first.
2708    pub fn tdz_marker(&mut self) -> Value {
2709        if let Some(v) = &self.tdz {
2710            return v.clone();
2711        }
2712        let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2713        self.tdz = Some(v.clone());
2714        v
2715    }
2716
2717    /// Whether `v` is the uninitialized-binding marker.
2718    pub fn is_tdz(&self, v: &Value) -> bool {
2719        matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2720    }
2721
2722    /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2723    /// already binds it. Emitted at the top of every scope for each `let`,
2724    /// `const` and `class` declared directly in it, so a read before the
2725    /// declaration throws instead of finding an OUTER binding of the same name —
2726    /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2727    pub fn hoist_tdz(&mut self, name: &str) {
2728        let marker = self.tdz_marker();
2729        let f = self.frame();
2730        // At module top level a lexical binding lives in `globals`, which is ALSO
2731        // what backs `globalThis.<name>` — so parking the marker there exposes it
2732        // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2733        // back as the marker. Top-level dead zones are tracked in a separate set
2734        // that only the name-read path consults.
2735        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2736            if !self.globals.contains_key(name) {
2737                self.tdz_globals.insert(name.to_string());
2738            }
2739            return;
2740        }
2741        let env = self.cur_env();
2742        let mut e = env.borrow_mut();
2743        if !e.vars.contains_key(name) {
2744            e.vars.insert(name.to_string(), marker);
2745        }
2746    }
2747
2748    /// Declare a new binding in the current scope (`let`/`const`). At the top of
2749    /// the module frame there is no local env, so those names become globals; once
2750    /// a block scope is open the binding belongs to that block.
2751    pub fn declare_name(&mut self, name: &str, val: Value) {
2752        let f = self.frame();
2753        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2754            self.tdz_globals.remove(name);
2755            self.globals.insert(name.to_string(), val);
2756        } else {
2757            self.cur_env()
2758                .borrow_mut()
2759                .vars
2760                .insert(name.to_string(), val);
2761        }
2762    }
2763
2764    /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2765    /// skips every open block scope and lands in the activation's base env.
2766    /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2767    /// name is not already bound in this activation.
2768    ///
2769    /// `var` bindings come into existence when the scope is entered, not where
2770    /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2771    /// rather than throwing. "If absent" is what keeps a parameter intact: in
2772    /// `function f(a) { var a; }` the `var` names a binding that already exists
2773    /// and must not be reset, which is also why a bare `var x;` emits nothing at
2774    /// its own position.
2775    pub fn hoist_var_name(&mut self, name: &str) {
2776        // The ENTRY script's top level is a CommonJS module body, not global
2777        // scope: node wraps every file in a function, so a top-level `var` is a
2778        // local of that wrapper. Binding it into the globals map made
2779        // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2780        // node says `undefined` — a REQUIRED module already ran inside a real
2781        // frame and behaved correctly, so only the entry file differed.
2782        if self.frame().is_module && !self.module_scope {
2783            self.globals.entry(name.to_string()).or_insert(Value::Undef);
2784            return;
2785        }
2786        let base = self.frame().base_env.clone();
2787        let mut env = base.borrow_mut();
2788        if !env.vars.contains_key(name) {
2789            env.vars.insert(name.to_string(), Value::Undef);
2790        }
2791    }
2792
2793    pub fn declare_var_name(&mut self, name: &str, val: Value) {
2794        if self.frame().is_module && !self.module_scope {
2795            self.globals.insert(name.to_string(), val);
2796            return;
2797        }
2798        let base = self.frame().base_env.clone();
2799        base.borrow_mut().vars.insert(name.to_string(), val);
2800    }
2801
2802    /// Enter a fresh block scope.
2803    pub fn push_scope(&mut self) {
2804        let env = self.cur_env();
2805        self.frames.last_mut().unwrap().env = child_env(env);
2806    }
2807
2808    /// Open a scope that is also the activation's VARIABLE environment, and
2809    /// return the previous one so the caller can restore it.
2810    ///
2811    /// A block scope is not enough for a strict direct `eval`: `var` and a
2812    /// hoisted function declaration bind to `base_env`, so they walked straight
2813    /// past a plain `push_scope` and still landed in the caller's function
2814    /// scope. Only `let`/`const` were contained.
2815    pub fn push_var_scope(&mut self) -> Env {
2816        let env = child_env(self.cur_env());
2817        let f = self.frames.last_mut().unwrap();
2818        let prev = std::mem::replace(&mut f.base_env, env.clone());
2819        f.env = env;
2820        prev
2821    }
2822
2823    /// Restore the variable environment a `push_var_scope` replaced.
2824    pub fn pop_var_scope(&mut self, prev: Env) {
2825        let f = self.frames.last_mut().unwrap();
2826        f.env = prev.clone();
2827        f.base_env = prev;
2828    }
2829
2830    /// Leave the innermost block scope (never pops past the activation's base).
2831    pub fn pop_scope(&mut self) {
2832        let cur = self.cur_env();
2833        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2834            return;
2835        }
2836        let parent = cur.borrow().parent.clone();
2837        if let Some(p) = parent {
2838            self.frames.last_mut().unwrap().env = p;
2839        }
2840    }
2841
2842    /// Replace the innermost block scope with a fresh copy of its bindings — the
2843    /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2844    /// in one iteration keeps that iteration's value.
2845    pub fn copy_scope(&mut self) {
2846        let cur = self.cur_env();
2847        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2848            return;
2849        }
2850        let parent = cur.borrow().parent.clone();
2851        let fresh = new_env(parent);
2852        fresh.borrow_mut().vars = cur.borrow().vars.clone();
2853        self.frames.last_mut().unwrap().env = fresh;
2854    }
2855
2856    /// The current block-scope env, for save/restore across a nested chunk.
2857    pub fn scope_snapshot(&self) -> Env {
2858        self.cur_env()
2859    }
2860    pub fn restore_scope(&mut self, env: Env) {
2861        self.frames.last_mut().unwrap().env = env;
2862    }
2863    pub fn set_global(&mut self, name: &str, val: Value) {
2864        self.globals.insert(name.to_string(), val);
2865    }
2866
2867    // ── output capture ───────────────────────────────────────────────────
2868    //
2869    // Every write a *program* makes — `console.log`, `process.stdout.write`,
2870    // `print` — funnels through `write_out`, so turning capture on redirects all
2871    // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2872    // crash traceback from `main`) deliberately do not: they belong to the
2873    // process, not to the program.
2874
2875    /// Start capturing program output in-process. Any text already captured is
2876    /// discarded, so each run starts clean.
2877    pub fn begin_capture(&mut self) {
2878        self.capture = Some(Vec::new());
2879    }
2880
2881    /// Stop capturing and take everything written since [`begin_capture`],
2882    /// returning the empty string when capture was not on. The captured bytes
2883    /// are rendered lossily: this API hands back a `String`, so a program that
2884    /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2885    /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2886    ///
2887    /// [`begin_capture`]: JsHost::begin_capture
2888    /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2889    pub fn end_capture(&mut self) -> String {
2890        String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2891    }
2892
2893    /// Stop capturing and take the raw bytes, without the lossy transcription
2894    /// [`end_capture`] applies.
2895    ///
2896    /// [`end_capture`]: JsHost::end_capture
2897    pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2898        self.capture.take().unwrap_or_default()
2899    }
2900
2901    /// Whether output is being captured — the one thing a caller needs to know
2902    /// before asking the real stream a question (`isTTY`, cursor position).
2903    pub fn capturing(&self) -> bool {
2904        self.capture.is_some()
2905    }
2906
2907    /// Write program output: into the capture buffer when capturing, else to the
2908    /// process stream `stderr` selects. `s` is written verbatim — callers add
2909    /// their own line ending, as `console.log` does and `process.stdout.write`
2910    /// does not.
2911    pub fn write_out(&mut self, s: &str, stderr: bool) {
2912        self.write_out_bytes(s.as_bytes(), stderr);
2913    }
2914
2915    /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2916    /// a byte string and Node writes it through untouched, so a `Buffer` holding
2917    /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2918    /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2919    /// became seven — so the byte path exists separately from [`write_out`].
2920    ///
2921    /// [`write_out`]: JsHost::write_out
2922    pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2923        if let Some(buf) = &mut self.capture {
2924            buf.extend_from_slice(bytes);
2925            return;
2926        }
2927        use std::io::Write as _;
2928        if stderr {
2929            let mut e = std::io::stderr();
2930            let _ = e.write_all(bytes);
2931            let _ = e.flush();
2932        } else {
2933            let mut o = std::io::stdout();
2934            let _ = o.write_all(bytes);
2935            let _ = o.flush();
2936        }
2937    }
2938    pub fn del_name(&mut self, name: &str) {
2939        if self
2940            .cur_env()
2941            .borrow_mut()
2942            .vars
2943            .shift_remove(name)
2944            .is_some()
2945        {
2946            return;
2947        }
2948        self.globals.shift_remove(name);
2949    }
2950
2951    pub fn current_this(&self) -> Option<Value> {
2952        self.frame().this_obj.clone()
2953    }
2954
2955    /// The running activation's [`ThisState`].
2956    pub fn this_state(&self) -> ThisState {
2957        self.frame().this_state
2958    }
2959
2960    /// Mark the next user-function activation as a derived constructor.
2961    pub fn mark_next_call_derived_ctor(&mut self) {
2962        self.derived_ctor_next = true;
2963    }
2964
2965    /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2966    /// nearest derived-constructor activation becomes `Bound`. That is the top
2967    /// frame, or — for `super()` inside an arrow — the constructor below the
2968    /// arrow's own frame. `false` when it was already bound: the second call.
2969    pub fn bind_super_this(&mut self) -> bool {
2970        let Some(f) = self
2971            .frames
2972            .iter_mut()
2973            .rev()
2974            .find(|f| f.this_state != ThisState::Plain)
2975        else {
2976            return true;
2977        };
2978        if f.this_state == ThisState::Bound {
2979            return false;
2980        }
2981        f.this_state = ThisState::Bound;
2982        true
2983    }
2984
2985    /// The object a `super()` call substituted for the instance, if any.
2986    ///
2987    /// `construct_class` allocates the instance up front, so when a base
2988    /// constructor RETURNS an object the substitution happens deep inside the
2989    /// VM, after that allocation. This carries it back out. Each
2990    /// `construct_class` saves and restores the previous value around its own
2991    /// run, so a `new` inside a constructor body cannot steal it.
2992    pub fn take_super_replacement(&mut self) -> Option<Value> {
2993        self.super_replacement.take()
2994    }
2995
2996    pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2997        std::mem::replace(&mut self.super_replacement, v)
2998    }
2999
3000    /// Rebind the running activation's `this`.
3001    ///
3002    /// Only `super()` does this: when the parent constructor RETURNS an object,
3003    /// 15.7.15 makes that object the derived instance, so the rest of the
3004    /// derived constructor has to write to it rather than to the one allocated
3005    /// before the call.
3006    pub fn set_current_this(&mut self, v: Value) {
3007        if let Some(f) = self.frames.last_mut() {
3008            f.this_obj = Some(v.clone());
3009        }
3010        self.super_replacement = Some(v);
3011    }
3012    /// The callbacks to run for `event`, consuming any `once` registration in
3013    /// the same step — so a listener that re-emits the event cannot re-enter a
3014    /// one-shot handler.
3015    pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
3016        let Some(list) = self.process_listeners.get_mut(event) else {
3017            return Vec::new();
3018        };
3019        let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
3020        list.retain(|l| !l.once);
3021        fired
3022    }
3023
3024    /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
3025    ///
3026    /// Node answers differently per entry point and both answers are objects:
3027    /// `node f.js` runs a CommonJS module, so top-level `this` is
3028    /// `module.exports`; `node -e` and `node -` run a Script, so it is
3029    /// `globalThis`. Verified on node v26.7.0 —
3030    /// `console.log(this === globalThis, this === module.exports)` is
3031    /// `false true` from a file and `true false` from `-e` and from stdin. It
3032    /// was `undefined` at every entry point here, so `this.x = 1` at module
3033    /// scope threw instead of populating the exports object.
3034    ///
3035    /// Only the base frame is touched: a plain function call still gets its own
3036    /// (`undefined`) binding rather than inheriting this one.
3037    pub fn set_top_this(&mut self, v: Value) {
3038        if let Some(f) = self.frames.first_mut() {
3039            f.this_obj = Some(v);
3040        }
3041    }
3042    pub fn current_env_capture(&self) -> Env {
3043        self.frame().env.clone()
3044    }
3045    pub fn current_new_target(&self) -> Option<Value> {
3046        self.frame().new_target.clone()
3047    }
3048    fn current_home_class(&self) -> Option<Value> {
3049        self.frame().home_class.clone()
3050    }
3051
3052    /// The `(parent_ctor, this_class_fields)` for a running constructor's
3053    /// `super(...)`, derived from the frame's home class.
3054    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3055        match self.current_home_class() {
3056            Some(cv) => match self.get(&cv) {
3057                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3058                _ => (None, Vec::new()),
3059            },
3060            None => (None, Vec::new()),
3061        }
3062    }
3063
3064    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3065    /// by the caller, outside any host borrow) or a directly-usable value.
3066    pub fn super_resolve(&self, name: &str) -> SuperRef {
3067        // A shorthand method in an OBJECT LITERAL resolves `super` through its
3068        // home object's prototype; only a class method has a home CLASS. With
3069        // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3070        // parent to look in and reported the method missing.
3071        if let Some(home) = self.frame().home_object.clone() {
3072            let target = self.proto_of(&home).unwrap_or(Value::Undef);
3073            if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3074                return SuperRef::Getter(getter);
3075            }
3076            return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3077        }
3078        let parent = match self
3079            .current_home_class()
3080            .and_then(|cv| match self.get(&cv) {
3081                Some(JsObj::Class(c)) => c.parent.clone(),
3082                _ => None,
3083            }) {
3084            Some(p) => p,
3085            None => return SuperRef::Data(Value::Undef),
3086        };
3087        // A STATIC method's home object is the constructor, so `super.x` reads
3088        // off the parent CONSTRUCTOR; an instance method's is the prototype
3089        // object, so it reads off the parent's prototype. Always taking the
3090        // prototype meant `static s() { return super.s(); }` found nothing and
3091        // then tried to call it.
3092        let target = if self.frame().home_static {
3093            parent.clone()
3094        } else {
3095            match self.get(&parent) {
3096                Some(JsObj::Class(pc)) => pc.proto.clone(),
3097                _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3098            }
3099        };
3100        if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3101            return SuperRef::Getter(getter);
3102        }
3103        if let Some(v) = lookup_chain(self, &target, name) {
3104            return SuperRef::Data(v);
3105        }
3106        // A static method lives in the fn-prop side table, not the property map.
3107        SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3108    }
3109
3110    // ── signals / errors ─────────────────────────────────────────────────
3111    pub fn take_error(&mut self) -> Option<String> {
3112        self.error.take()
3113    }
3114    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3115        let s = if msg.is_empty() {
3116            class.to_string()
3117        } else {
3118            format!("{class}: {msg}")
3119        };
3120        self.error = Some(s.clone());
3121        s
3122    }
3123}
3124
3125// ── error constructors ───────────────────────────────────────────────────────
3126
3127pub fn type_error(msg: &str) -> String {
3128    format!("TypeError: {msg}")
3129}
3130pub fn ref_error(name: &str) -> String {
3131    format!("ReferenceError: {name} is not defined")
3132}
3133
3134/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3135/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3136/// happened, and the difference is how a reader tells a misspelled name from a
3137/// `let` used above its declaration.
3138pub fn tdz_error(name: &str) -> String {
3139    format!("ReferenceError: Cannot access '{name}' before initialization")
3140}
3141pub fn range_error(msg: &str) -> String {
3142    format!("RangeError: {msg}")
3143}
3144
3145/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3146/// largest string the engine will materialize.
3147///
3148/// Measured on node v26.7.0 (darwin arm64):
3149/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3150/// `'a'.repeat(536870888)` succeeds with that length, and
3151/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3152pub const MAX_STRING_LENGTH: usize = 536_870_888;
3153
3154/// The error V8 raises for a string operation whose RESULT would exceed
3155/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3156/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3157/// sit building a 1 TiB `String` until it was killed.
3158pub fn invalid_string_length() -> String {
3159    range_error("Invalid string length")
3160}
3161
3162/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3163/// and 10.4.2.4 `ArraySetLength` step 3.
3164///
3165/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3166/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3167/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3168/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3169/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3170/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3171/// materializing four billion elements for `a.length = 2**32`.
3172pub fn to_array_length(v: &Value) -> Result<usize, String> {
3173    // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3174    // `ToNumber(value)`, compared against each other. Both are observable — a
3175    // counting `valueOf` sees two calls in node and saw one here — and the
3176    // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3177    let u32_pass = to_number_value(v)?;
3178    let n = to_number_value(v)?;
3179    let _ = u32_pass;
3180    // `ToUint32`: truncate toward zero, then modulo 2^32.
3181    let u = if n.is_finite() {
3182        (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3183    } else {
3184        0
3185    };
3186    // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3187    if (u as f64) != n {
3188        return Err(range_error("Invalid array length"));
3189    }
3190    Ok(u as usize)
3191}
3192
3193/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3194///
3195/// `builtins::synth_error` parses that head back apart, so the bracketed code
3196/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3197/// handling reads. Writing the head by hand at each throw site is what left a
3198/// dozen of them with `err.code === undefined` while the message matched.
3199///
3200/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3201/// is left bracketed while the stack is captured and therefore shows up in both
3202/// `String(err)` and `err.stack` — measured on v26.7.0:
3203///
3204/// ```text
3205/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3206/// ```
3207pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3208    format!("{class} [{code}]: {msg}")
3209}
3210
3211/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3212pub const CODE_MARK: &str = "\u{1}code:";
3213
3214/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3215/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3216/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3217/// `synth_error` could otherwise recognise.
3218pub const DOM_MARK: &str = "\u{1}dom:";
3219
3220/// A `DOMException` error string: `name` is the WHATWG error name.
3221pub fn dom_error(name: &str, msg: &str) -> String {
3222    format!("{DOM_MARK}{name}\u{1}{msg}")
3223}
3224
3225/// A Node coded error raised from the *native* layer: `.code` is set, but the
3226/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3227///
3228/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3229/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3230/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3231/// brackets its code into the very same two reads. Encoding both through one
3232/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3233///
3234/// The code rides in a marker at the head of the message rather than in the
3235/// error class, because the class text is exactly what must NOT carry it. The
3236/// marker is an internal wire format between a throw site and `synth_error`; it
3237/// never survives into a `.message`.
3238pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3239    format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3240}
3241
3242/// Marks the start of the extra string own properties a
3243/// [`plain_coded_error_with`] error carries after its message.
3244pub const FIELDS_MARK: char = '\u{2}';
3245
3246/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3247/// `code` in the order given — `new URL('x', 'nope')` throws with
3248/// `Object.keys(e)` reading `["code","input","base"]`.
3249///
3250/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3251/// arbitrary user text) may carry any character, the separators included.
3252pub fn plain_coded_error_with(
3253    class: &str,
3254    code: &str,
3255    msg: &str,
3256    fields: &[(&str, &str)],
3257) -> String {
3258    let mut s = plain_coded_error(class, code, msg);
3259    s.push(FIELDS_MARK);
3260    for (k, v) in fields {
3261        s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3262    }
3263    s
3264}
3265
3266/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3267/// internal code and field markers of [`plain_coded_error`] /
3268/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3269/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3270pub fn plain_error_text(e: &str) -> String {
3271    let Some(i) = e.find(CODE_MARK) else {
3272        return e.to_string();
3273    };
3274    let (head, rest) = e.split_at(i);
3275    match rest[CODE_MARK.len()..].split_once('\u{1}') {
3276        Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3277        None => e.to_string(),
3278    }
3279}
3280
3281/// Split a [`plain_coded_error_with`] message back into the message and its
3282/// fields. A message with no field mark comes back whole with no fields.
3283pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3284    let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3285        return (msg, Vec::new());
3286    };
3287    let mut fields = Vec::new();
3288    while let Some((k, tail)) = rest.split_once('\u{3}') {
3289        let Some((len, tail)) = tail.split_once('\u{3}') else {
3290            break;
3291        };
3292        let Ok(len) = len.parse::<usize>() else { break };
3293        let Some(v) = tail.get(..len) else { break };
3294        fields.push((k, v));
3295        rest = &tail[len..];
3296    }
3297    (head, fields)
3298}
3299
3300/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3301/// <expected>. Received …` — Node's single most common argument rejection.
3302pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3303    coded_error(
3304        "TypeError",
3305        "ERR_INVALID_ARG_TYPE",
3306        &format!(
3307            "The \"{name}\" {kind} must be of type {expected}. Received {}",
3308            crate::stdlib::received_desc(v)
3309        ),
3310    )
3311}
3312
3313// ── the fusevm run plumbing ──────────────────────────────────────────────────
3314
3315thread_local! {
3316    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3317}
3318
3319/// Enable/disable DAP debug execution (`node --dap`).
3320pub fn set_debug_mode(on: bool) {
3321    DEBUG_MODE.with(|d| d.set(on));
3322}
3323
3324// ── join cycle detection ─────────────────────────────────────────────────────
3325
3326thread_local! {
3327    /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3328    static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3329}
3330
3331/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3332/// it already is.
3333///
3334/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3335/// it) is the one place the language walks an object graph with no depth bound,
3336/// so every engine cuts re-entrance here: a receiver already on the stack
3337/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3338/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3339/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3340/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3341/// uncatchable, where node returns a string.
3342///
3343/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3344/// twice, because the first render pops before the second pushes.
3345///
3346/// A `true` return MUST be paired with [`join_stack_pop`].
3347pub fn join_stack_push(v: &Value) -> bool {
3348    match v {
3349        Value::Obj(i) => JOIN_STACK.with(|s| {
3350            let mut s = s.borrow_mut();
3351            if s.contains(i) {
3352                false
3353            } else {
3354                s.push(*i);
3355                true
3356            }
3357        }),
3358        _ => true,
3359    }
3360}
3361
3362/// Pop the innermost [`join_stack_push`].
3363pub fn join_stack_pop() {
3364    JOIN_STACK.with(|s| {
3365        s.borrow_mut().pop();
3366    });
3367}
3368
3369// ── native stack guard ───────────────────────────────────────────────────────
3370
3371thread_local! {
3372    /// Lowest stack address a nested run may start from, or 0 before the
3373    /// running thread's bounds have been measured. Cached because the pthread
3374    /// query is a syscall-free but non-trivial read and this is on every call.
3375    static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3376}
3377
3378/// Stack left unusable below the floor, as a fraction of the whole stack: the
3379/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3380/// whatever `catch` receives it, all of which needs room *below* the deepest
3381/// call that was allowed.
3382const STACK_RESERVE_DIVISOR: usize = 8;
3383/// Floor of that reserve, for a thread whose stack is small enough that an
3384/// eighth of it would not cover the unwind.
3385const STACK_RESERVE_MIN: usize = 512 * 1024;
3386/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3387/// large relative to a default 8 MiB stack — over-reserving costs recursion
3388/// depth, under-reserving costs the process.
3389const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3390
3391/// The address of a local in the caller's frame — how far down the stack
3392/// execution currently is. `black_box` keeps the probe from being optimized into
3393/// a different frame.
3394fn stack_pointer() -> usize {
3395    let probe = 0u8;
3396    std::hint::black_box(&probe) as *const u8 as usize
3397}
3398
3399/// The running thread's `(lowest address, size)` stack bounds.
3400///
3401/// Asked of pthread rather than assumed, because the three threads that run JS
3402/// have three different stacks: the `node` binary's own (`main.rs` reserves
3403/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3404/// harness thread's. A fixed byte budget would be wrong on two of the three.
3405fn stack_bounds() -> Option<(usize, usize)> {
3406    #[cfg(target_vendor = "apple")]
3407    {
3408        // SAFETY: both calls are pure reads of the calling thread's own
3409        // pthread record; neither allocates nor can fail.
3410        unsafe {
3411            let me = libc::pthread_self();
3412            let top = libc::pthread_get_stackaddr_np(me) as usize;
3413            let size = libc::pthread_get_stacksize_np(me);
3414            if size == 0 || top < size {
3415                return None;
3416            }
3417            Some((top - size, size))
3418        }
3419    }
3420    #[cfg(target_os = "linux")]
3421    {
3422        // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3423        // read, only read on the success path, and destroyed on every path.
3424        unsafe {
3425            let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3426            if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3427                return None;
3428            }
3429            let mut low: *mut libc::c_void = std::ptr::null_mut();
3430            let mut size: libc::size_t = 0;
3431            let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3432            libc::pthread_attr_destroy(&mut attr);
3433            if ok && size != 0 {
3434                return Some((low as usize, size));
3435            }
3436            None
3437        }
3438    }
3439    #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3440    {
3441        None
3442    }
3443}
3444
3445/// The stack address below which a further nested VM run must throw instead of
3446/// recursing.
3447///
3448/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3449/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3450/// and runs the body, whose own calls land back here. Unbounded JS recursion
3451/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3452/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3453/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3454/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3455///
3456/// The floor is derived from the thread's real bounds rather than a frame count
3457/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3458/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3459/// stack and no more), a release build far less, and a native builtin recursing
3460/// through a user callback spends a different amount again.
3461fn stack_floor() -> usize {
3462    let cached = STACK_FLOOR.with(|c| c.get());
3463    if cached != 0 {
3464        return cached;
3465    }
3466    let floor = match stack_bounds() {
3467        Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3468        None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3469    };
3470    STACK_FLOOR.with(|c| c.set(floor));
3471    floor
3472}
3473
3474/// Stack given to each generator/async coroutine.
3475///
3476/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3477/// left a `function*` body barely ten frames of recursion before it walked off
3478/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3479/// a larger one is address space, not resident memory — but it IS per live
3480/// generator, so this stays far below the entry thread's
3481/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3482/// has thousands of these.
3483const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3484
3485/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3486fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3487    stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3488}
3489
3490/// corosensei's own `DefaultStack::default()` size, used only when the
3491/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3492/// on a stack whose bounds are not ours to read.
3493const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3494
3495/// Give a coroutine whose stack bounds are unknown a floor measured from where
3496/// its body starts. Called once, at body entry, on the coroutine's own stack.
3497fn ensure_coroutine_floor() {
3498    if STACK_FLOOR.with(|c| c.get()) != 0 {
3499        return;
3500    }
3501    let budget = CORO_FALLBACK_STACK_SIZE
3502        - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3503    STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3504}
3505
3506/// Install `floor` as the current stack floor, returning the previous one.
3507///
3508/// Used around a coroutine resume, which switches to a stack the thread's
3509/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3510/// the next [`stack_floor`] measure again, which is the right answer for the
3511/// entry thread and a conservative one for a fallback coroutine stack.
3512fn swap_stack_floor(floor: usize) -> usize {
3513    STACK_FLOOR.with(|c| c.replace(floor))
3514}
3515
3516/// Whether the native stack is too close to its floor for one more nested run.
3517pub fn stack_exhausted() -> bool {
3518    stack_pointer() <= stack_floor()
3519}
3520
3521/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3522/// `RangeError` constructor node uses — not a `panic!`.
3523pub fn stack_overflow_error() -> String {
3524    range_error("Maximum call stack size exceeded")
3525}
3526
3527/// Pool key for the body of user function `def_id`.
3528pub fn func_key(def_id: usize) -> u64 {
3529    1 << 40 | def_id as u64
3530}
3531
3532/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3533/// handler, 2 = the finalizer.
3534pub fn try_key(try_id: usize, part: u64) -> u64 {
3535    2 << 40 | (try_id as u64) << 2 | part
3536}
3537
3538thread_local! {
3539    /// VMs that have finished a run, kept for the next one — grouped by the
3540    /// chunk they still hold.
3541    ///
3542    /// Every JS call, every `try` block and every generator step runs its chunk
3543    /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3544    /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3545    /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3546    /// 400k calls, so it built 400k VMs to run 23 ops each.
3547    ///
3548    /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3549    /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3550    /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3551    /// call takes back the VM that already holds that body and copies nothing:
3552    /// `VM::reset` is handed the chunk the VM was already carrying.
3553    ///
3554    /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3555    /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3556    /// a nested (or recursive) call takes the next one, so a key grows to the
3557    /// deepest simultaneous entry into that function and no further.
3558    static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3559        RefCell::new(rustc_hash::FxHashMap::default());
3560}
3561
3562/// An idle VM filed under `key`, if any.
3563fn take_pooled(key: u64) -> Option<VM> {
3564    VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3565}
3566
3567/// File a finished VM under `key` for the next run to take.
3568fn put_pooled(key: u64, vm: VM) {
3569    VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3570}
3571
3572/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3573/// and fitted with the builtins and hooks a fresh VM needs.
3574fn acquire_vm(chunk: Chunk) -> VM {
3575    if let Some(mut vm) = take_pooled(0) {
3576        vm.reset(chunk);
3577        return vm;
3578    }
3579    let mut vm = VM::new(chunk);
3580    crate::builtins::install(&mut vm);
3581    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3582        crate::builtins::numeric_hook(op, a, b)
3583    }));
3584    // Under `--dap` the tracing JIT would compile hot loops and skip the
3585    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3586    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3587    // extension seam mirrors pythonrs should the marker emission ever switch.
3588    // The mode is fixed before the first chunk runs, so a pooled VM can never
3589    // come back wearing the wrong one.
3590    if DEBUG_MODE.with(|d| d.get()) {
3591        vm.set_extension_handler(Box::new(|vm, id, _| {
3592            crate::dap::on_ext(vm, id);
3593        }));
3594    } else {
3595        vm.enable_tracing_jit();
3596    }
3597    vm
3598}
3599
3600/// Register every node-js builtin + the numeric hook on a VM, then run it.
3601///
3602/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3603/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3604/// that runs repeatedly (a function body, a `try` block) goes through
3605/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3606pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3607    // Checked before the `VM` is built: `VM::new` + `install` are themselves
3608    // several KiB of frame, so a check after them could already have overflowed.
3609    if stack_exhausted() {
3610        return Err(stack_overflow_error());
3611    }
3612    finish_run(0, acquire_vm(chunk))
3613}
3614
3615/// Run the chunk filed under `key`, building it with `make` only if no VM is
3616/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3617/// call copies no bytecode at all.
3618pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3619    if stack_exhausted() {
3620        return Err(stack_overflow_error());
3621    }
3622    let vm = match take_pooled(key) {
3623        Some(mut vm) => {
3624            // Hand the VM back the chunk it is already carrying: `reset` takes
3625            // an owned `Chunk`, and this is the one place where the owned chunk
3626            // costs nothing.
3627            let held = std::mem::take(&mut vm.chunk);
3628            vm.reset(held);
3629            vm
3630        }
3631        None => acquire_vm(make()),
3632    };
3633    finish_run(key, vm)
3634}
3635
3636/// Run a prepared VM to completion and file it back under `key`.
3637fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3638    let outcome = vm.run();
3639    let result = match outcome {
3640        _ if with_host(|h| h.error.is_some()) => {
3641            Err(with_host(|h| h.take_error()).expect("just checked"))
3642        }
3643        VMResult::Ok(v) => Ok(v),
3644        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3645        VMResult::Error(e) => Err(e),
3646    };
3647    put_pooled(key, vm);
3648    result
3649}
3650
3651/// Run `chunk` in the GLOBAL scope instead of the caller's.
3652///
3653/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3654/// and can shadow — the *calling function's* locals. That is right for a direct
3655/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3656/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3657/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3658/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3659/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3660/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3661/// is `"undefined"` there and was `"number"` here.
3662///
3663/// A `var` the chunk itself declares lands in the top-level scope and persists,
3664/// so successive `vm.runInThisContext` calls share it.
3665pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3666    // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3667    // to the global object, not to the entry module's wrapper scope. The flag
3668    // that keeps the entry script's own `var`s out of the globals map has to be
3669    // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3670    // `globalThis.g`.
3671    let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3672    let out = run_chunk_in_global_scope_inner(chunk);
3673    with_host(|h| h.module_scope = prev_scope);
3674    out
3675}
3676
3677fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3678    let global_env = with_host(|h| h.global_env.clone());
3679    with_host(|h| {
3680        h.frames.push(Frame {
3681            env: global_env.clone(),
3682            base_env: global_env,
3683            this_obj: None,
3684            new_target: None,
3685            home_class: None,
3686            home_static: false,
3687            home_object: None,
3688            strict: false,
3689            line: 0,
3690            owner: None,
3691            is_module: true,
3692            this_state: ThisState::Plain,
3693        })
3694    });
3695    let r = run_chunk_on(chunk);
3696    with_host(|h| {
3697        h.frames.pop();
3698    });
3699    r
3700}
3701
3702/// Run the top-level program chunk, then drain the event loop (microtasks +
3703/// timers) until quiescent — matching Node, which keeps the process alive while
3704/// pending async work remains.
3705pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3706    with_host(|h| h.module_scope = true);
3707    let r = run_chunk_on(chunk);
3708    with_host(|h| h.signal = None);
3709    if r.is_ok() {
3710        run_event_loop()?;
3711        finish_process_events()?;
3712    }
3713    r
3714}
3715
3716/// The shutdown sequence Node runs once the loop has drained on its own: fire
3717/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3718/// again and `beforeExit` fires again), then fire `exit` exactly once.
3719///
3720/// Neither event fired at all before this existed, so `process.on('exit', …)`
3721/// was a registration with no delivery — a listener whose body printed was
3722/// silently dropped, and one that set `process.exitCode` could not affect the
3723/// status. Measured on node v26.7.0,
3724/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3725///
3726/// An explicit `process.exit()` never reaches here (it leaves the process from
3727/// inside the builtin), and neither does an uncaught exception — matching
3728/// Node, where `beforeExit` is skipped on both paths.
3729fn finish_process_events() -> Result<(), String> {
3730    // Bounded: a `beforeExit` listener that re-arms work every time would spin
3731    // forever, exactly as it does in Node, but a runaway here would hang a
3732    // parity run with no output, so it is capped and then treated as drained.
3733    for _ in 0..1000 {
3734        let code = with_host(|h| h.exit_code).unwrap_or(0);
3735        if !crate::stdlib::process::emit_before_exit(code)? {
3736            break;
3737        }
3738        let more =
3739            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3740        if !more {
3741            break;
3742        }
3743        run_event_loop()?;
3744    }
3745    let code = with_host(|h| h.exit_code).unwrap_or(0);
3746    crate::stdlib::process::emit_exit_event(code)
3747}
3748
3749// ── formatting ───────────────────────────────────────────────────────────────
3750
3751/// Format a JS number exactly as `Number.prototype.toString` does for the common
3752/// range (no exponential-notation threshold handling for very large/small).
3753pub fn fmt_number(f: f64) -> String {
3754    if f.is_nan() {
3755        return "NaN".into();
3756    }
3757    if f.is_infinite() {
3758        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3759    }
3760    if f == 0.0 {
3761        // Covers -0.0 too: (-0).toString() === "0".
3762        return "0".into();
3763    }
3764    if f < 0.0 {
3765        return format!("-{}", js_number_repr(-f));
3766    }
3767    js_number_repr(f)
3768}
3769
3770/// If `k` is an array-index property key, return its numeric value. Per
3771/// ECMAScript, a String property key `P` is an array index iff
3772/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3773/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3774pub fn array_index(k: &str) -> Option<u32> {
3775    if k.is_empty() {
3776        return None;
3777    }
3778    if k == "0" {
3779        return Some(0);
3780    }
3781    // A leading '0' (other than the lone "0" above) is non-canonical.
3782    if k.as_bytes()[0] == b'0' {
3783        return None;
3784    }
3785    if !k.bytes().all(|b| b.is_ascii_digit()) {
3786        return None;
3787    }
3788    match k.parse::<u64>() {
3789        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3790        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3791        _ => None,
3792    }
3793}
3794
3795/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3796/// integer-index keys sort ascending-numeric and precede all string keys; two
3797/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3798/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3799/// they also fall into the stable-insertion-order tail.)
3800pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3801    use std::cmp::Ordering;
3802    match (array_index(a), array_index(b)) {
3803        (Some(x), Some(y)) => x.cmp(&y),
3804        (Some(_), None) => Ordering::Less,
3805        (None, Some(_)) => Ordering::Greater,
3806        (None, None) => Ordering::Equal,
3807    }
3808}
3809
3810/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3811/// place: array-index keys ascending first, then the remaining keys in their
3812/// existing (insertion) order. A no-op unless at least one index key is present,
3813/// so the overwhelmingly common all-string-key object keeps its exact order and
3814/// pays nothing. `IndexMap::sort_by` is a stable sort.
3815pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3816    if props.keys().any(|k| array_index(k).is_some()) {
3817        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3818    }
3819}
3820
3821/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3822///
3823/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3824/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3825/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3826/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3827/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3828/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3829/// `n > 21` or `n ≤ -6`.
3830fn js_number_repr(a: f64) -> String {
3831    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3832    // round-trip digits. Split it into the digit string `s` and exponent `E`.
3833    let sci = format!("{a:e}");
3834    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3835    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3836    let s: String = mant.chars().filter(|c| *c != '.').collect();
3837    let k = s.len() as i32; // number of significant digits
3838    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3839
3840    if k <= n && n <= 21 {
3841        // Integer with trailing zeros: all digits, then n−k zeros.
3842        let mut out = s;
3843        out.push_str(&"0".repeat((n - k) as usize));
3844        out
3845    } else if 0 < n && n <= 21 {
3846        // Decimal point inside the digit run: n digits, '.', the rest.
3847        format!("{}.{}", &s[..n as usize], &s[n as usize..])
3848    } else if -6 < n && n <= 0 {
3849        // Leading "0." then (−n) zeros then all digits.
3850        format!("0.{}{}", "0".repeat((-n) as usize), s)
3851    } else {
3852        // Exponential form. Exponent digit is n−1, always signed.
3853        let exp = n - 1;
3854        let sign = if exp >= 0 { '+' } else { '-' };
3855        let mag = exp.abs();
3856        if k == 1 {
3857            format!("{s}e{sign}{mag}")
3858        } else {
3859            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3860        }
3861    }
3862}
3863
3864impl JsHost {
3865    /// The `typeof` string for `v`.
3866    pub fn type_of(&self, v: &Value) -> &'static str {
3867        match v {
3868            Value::Undef => "undefined",
3869            Value::Bool(_) => "boolean",
3870            Value::Int(_) | Value::Float(_) => "number",
3871            Value::Str(_) => "string",
3872            Value::Obj(_) => match self.get(v) {
3873                Some(JsObj::Str(_)) => "string",
3874                // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3875                // target is callable, so `typeof` classifies by the target —
3876                // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3877                // is bounded: a proxy of a proxy defers again.
3878                Some(JsObj::Proxy { target, .. }) => {
3879                    let mut cur = target;
3880                    for _ in 0..100 {
3881                        match self.get(cur) {
3882                            Some(JsObj::Proxy { target: t, .. }) => cur = t,
3883                            _ => break,
3884                        }
3885                    }
3886                    if is_callable(self, cur) {
3887                        "function"
3888                    } else {
3889                        "object"
3890                    }
3891                }
3892                Some(JsObj::Func(_))
3893                | Some(JsObj::BoundMethod { .. })
3894                | Some(JsObj::BoundFunc { .. })
3895                | Some(JsObj::Class(_)) => "function",
3896                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3897                // `typeof === "function"` — EXCEPT the non-callable namespace
3898                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3899                Some(JsObj::Builtin(n)) => {
3900                    if builtin_is_callable(n) {
3901                        "function"
3902                    } else {
3903                        "object"
3904                    }
3905                }
3906                Some(JsObj::Symbol { .. }) => "symbol",
3907                Some(JsObj::BigInt(_)) => "bigint",
3908                _ => "object", // arrays, objects, null, Map/Set, generators
3909            },
3910            _ => "object",
3911        }
3912    }
3913
3914    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3915    pub fn truthy(&self, v: &Value) -> bool {
3916        match v {
3917            Value::Undef => false,
3918            Value::Bool(b) => *b,
3919            Value::Int(n) => *n != 0,
3920            Value::Float(f) => *f != 0.0 && !f.is_nan(),
3921            Value::Str(s) => !s.is_empty(),
3922            Value::Obj(_) => match self.get(v) {
3923                Some(JsObj::Str(s)) => !s.is_empty(),
3924                Some(JsObj::Null) => false,
3925                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3926                _ => true, // arrays, objects, functions
3927            },
3928            _ => true,
3929        }
3930    }
3931
3932    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3933    pub fn to_number(&self, v: &Value) -> f64 {
3934        match v {
3935            Value::Undef => f64::NAN,
3936            Value::Bool(b) => {
3937                if *b {
3938                    1.0
3939                } else {
3940                    0.0
3941                }
3942            }
3943            Value::Int(n) => *n as f64,
3944            Value::Float(f) => *f,
3945            Value::Str(s) => str_to_number(s),
3946            Value::Obj(_) => match self.get(v) {
3947                Some(JsObj::Str(s)) => str_to_number(s),
3948                Some(JsObj::Null) => 0.0,
3949                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3950                Some(JsObj::Array(items)) => {
3951                    // [] -> 0, [x] -> ToNumber(x), else NaN.
3952                    if items.is_empty() {
3953                        0.0
3954                    } else if items.len() == 1 {
3955                        self.to_number(&items[0])
3956                    } else {
3957                        f64::NAN
3958                    }
3959                }
3960                _ => f64::NAN,
3961            },
3962            _ => f64::NAN,
3963        }
3964    }
3965
3966    /// `String(v)` — the string-coercion form (raw, unquoted).
3967    pub fn str_of(&self, v: &Value) -> String {
3968        match v {
3969            Value::Undef => "undefined".into(),
3970            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3971            Value::Int(n) => n.to_string(),
3972            Value::Float(f) => fmt_number(*f),
3973            Value::Str(s) => (**s).clone(),
3974            Value::Obj(_) => match self.get(v) {
3975                Some(JsObj::Str(s)) => s.clone(),
3976                Some(JsObj::Null) => "null".into(),
3977                Some(JsObj::BigInt(b)) => b.to_string(),
3978                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3979                Some(JsObj::Array(items)) => {
3980                    // Array.prototype.toString: comma-join, null/undefined -> "".
3981                    // Guarded by the JoinStack (see `join_stack_push`) so a
3982                    // self-referential array yields "" instead of recursing until
3983                    // the native stack aborts the process.
3984                    if !join_stack_push(v) {
3985                        return String::new();
3986                    }
3987                    let parts: Vec<String> = items
3988                        .iter()
3989                        .map(|x| match x {
3990                            Value::Undef => String::new(),
3991                            _ if self.is_null(x) => String::new(),
3992                            _ => self.str_of(x),
3993                        })
3994                        .collect();
3995                    join_stack_pop();
3996                    parts.join(",")
3997                }
3998                Some(JsObj::Object(props)) => {
3999                    // A native `Buffer` stringifies to its decoded (utf-8)
4000                    // contents, matching `buf.toString()` — needed for `'' + buf`,
4001                    // template interpolation, and `data += chunk` (the pattern
4002                    // Express/body-parser use to read a request body).
4003                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
4004                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4005                            Some(JsObj::Array(items)) => {
4006                                items.iter().map(|x| self.to_number(x) as u8).collect()
4007                            }
4008                            _ => Vec::new(),
4009                        };
4010                        String::from_utf8_lossy(&bytes).into_owned()
4011                    } else if let Some(s) = self.error_to_string(v) {
4012                        s
4013                    } else {
4014                        "[object Object]".into()
4015                    }
4016                }
4017                Some(JsObj::Func(f)) => {
4018                    // A function built from runtime source (`new Function`,
4019                    // `vm.compileFunction`) retains the exact text V8 synthesizes
4020                    // for it, so `Function.prototype.toString` reports what Node
4021                    // reports. Every other function slices its span out of the
4022                    // script it was parsed from; only one whose program kept no
4023                    // text (an AOT image, a `rust { }` desugared file) falls back
4024                    // to the placeholder.
4025                    if let Some(src) = self.fn_prop(v, "@@source") {
4026                        return self.str_of(&src);
4027                    }
4028                    if let Some(text) = self.func_source(f.def_id) {
4029                        return text.to_string();
4030                    }
4031                    let name = self
4032                        .funcs
4033                        .get(f.def_id)
4034                        .map(|d| d.name.clone())
4035                        .unwrap_or_default();
4036                    format!("function {name}() {{ [code] }}")
4037                }
4038                // The native-code form names the FUNCTION, not its key:
4039                // `String(Math.max)` is `function max() { [native code] }`.
4040                Some(JsObj::Builtin(n)) => {
4041                    // The `console` methods are the exception node itself makes:
4042                    // each is a wrapper, so `String(console.log)` is the
4043                    // ANONYMOUS native-code form even though `console.log.name`
4044                    // is `log`. Measured on v26.8.1.
4045                    if n.starts_with("console.") {
4046                        "function () { [native code] }".into()
4047                    } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
4048                        // An accessor half names itself `get size` / `set
4049                        // arguments`, which `builtin_name` cannot build because
4050                        // it returns a borrowed `&str`.
4051                        format!("function {accessor}() {{ [native code] }}")
4052                    } else {
4053                        format!(
4054                            "function {}() {{ [native code] }}",
4055                            crate::builtins::builtin_name(n)
4056                        )
4057                    }
4058                }
4059                // A method read off an instance names itself the same way the
4060                // prototype method it resolves to does: `String([].slice)` is
4061                // `function slice() { [native code] }`.
4062                Some(JsObj::BoundMethod { name, .. }) => {
4063                    format!("function {name}() {{ [native code] }}")
4064                }
4065                Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4066                // `Function.prototype.toString` refuses to expose a proxy's
4067                // target: V8 reports the native-code form for a proxy of ANY
4068                // callable, so `String(new Proxy(function f(){}, {}))` is
4069                // `function () { [native code] }`, not `f`'s source.
4070                Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4071                    "function () { [native code] }".into()
4072                }
4073                Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4074                    Some(text) => text.to_string(),
4075                    None => format!("class {} {{ }}", c.name),
4076                },
4077                Some(JsObj::Symbol { desc, .. }) => {
4078                    // `String(sym)` is allowed (unlike implicit coercion) and yields
4079                    // `Symbol(desc)`.
4080                    match desc {
4081                        Some(d) => format!("Symbol({d})"),
4082                        None => "Symbol()".into(),
4083                    }
4084                }
4085                _ => "[object Object]".into(),
4086            },
4087            _ => "[object Object]".into(),
4088        }
4089    }
4090
4091    /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4092    /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4093    /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4094    /// entry and showing it twice would be wrong.
4095    ///
4096    /// Only a DATA property is seen. A tag supplied by a prototype getter
4097    /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4098    /// which cannot run under the host borrow `inspect` holds — such an object
4099    /// prints without the prefix.
4100    /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4101    /// a primitive wrapper, distinguishing it from the bare primitive.
4102    fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4103        let prim = match self.get(v) {
4104            Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4105            _ => return None,
4106        };
4107        let ctor = match &prim {
4108            Value::Bool(_) => "Boolean",
4109            Value::Int(_) | Value::Float(_) => "Number",
4110            // BigInt and Symbol primitives live on the heap; their boxes are
4111            // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4112            _ => match self.get(&prim) {
4113                Some(JsObj::BigInt(_)) => "BigInt",
4114                Some(JsObj::Symbol { .. }) => "Symbol",
4115                _ => "String",
4116            },
4117        };
4118        let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4119        // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4120        // boxed characters are NOT extras — node hides the index properties of
4121        // a String wrapper, showing only what was added to it.
4122        let width = if ctor == "String" {
4123            self.str_of(&prim).chars().count()
4124        } else {
4125            0
4126        };
4127        let extras: Vec<String> = match self.get(v) {
4128            Some(JsObj::Object(p)) => p
4129                .iter()
4130                .filter(|(k, _)| {
4131                    !k.starts_with("@@")
4132                        && !k.starts_with('#')
4133                        && self.prop_attrs(v, k).enumerable
4134                        && !k.parse::<usize>().is_ok_and(|i| i < width)
4135                })
4136                .map(|(k, val)| {
4137                    format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4138                })
4139                .collect(),
4140            _ => Vec::new(),
4141        };
4142        if extras.is_empty() {
4143            return Some(head);
4144        }
4145        Some(self.render_object(&extras, &format!("{head} "), indent, st))
4146    }
4147
4148    /// The `key: value` parts for own properties a script attached to an exotic
4149    /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4150    /// prints `Map(1) { 'k' => 1, x: 5 }`.
4151    fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4152        self.fn_prop_keys(v)
4153            .into_iter()
4154            .filter(|k| {
4155                !k.starts_with("@@")
4156                    && !k.starts_with('#')
4157                    && !is_symbol_key(k)
4158                    && self.prop_attrs(v, k).enumerable
4159            })
4160            .map(|k| {
4161                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4162                format!(
4163                    "{}: {}",
4164                    fmt_key(&k),
4165                    self.inspect_lvl(&val, indent + 2, st)
4166                )
4167            })
4168            .collect()
4169    }
4170
4171    fn inspect_tag(&self, v: &Value) -> Option<String> {
4172        let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4173        if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4174            return None;
4175        }
4176        let t = lookup_chain(self, v, "@@toStringTag")?;
4177        self.as_str(&t)
4178    }
4179
4180    /// `console.log`-style rendering of a top-level argument: bare strings print
4181    /// raw; everything else uses `inspect`.
4182    pub fn console_format(&self, v: &Value) -> String {
4183        match v {
4184            Value::Str(_) => self.str_of(v),
4185            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4186            _ => self.inspect(v),
4187        }
4188    }
4189
4190    /// `util.inspect`-style rendering (nested; strings quoted).
4191    pub fn inspect(&self, v: &Value) -> String {
4192        self.inspect_lvl(v, 0, &mut InspectCycles::default())
4193    }
4194
4195    /// `util.inspect` at a given indentation level, with the cycle guard applied
4196    /// around the object cases.
4197    ///
4198    /// A value already being rendered further up the chain is a CYCLE, and Node
4199    /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4200    /// object it points back at is prefixed `<ref *N>`. Without this the walk
4201    /// only stopped when the depth limit turned the back-edge into `[Object]`,
4202    /// so `const c={a:1}; c.c=c` printed the misleading
4203    /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4204    /// `<ref *1> { a: 1, c: [Circular *1] }`.
4205    ///
4206    /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4207    /// the target's own children are being rendered — so the prefix can only be
4208    /// decided after `inspect_value` returns.
4209    /// Whether `v` renders as a LEAF — a finished string produced without
4210    /// recursing into any child.
4211    ///
4212    /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4213    /// after the early returns for the shapes that answer immediately: a bare
4214    /// Date is its ISO string, a regex is its literal, an empty container is its
4215    /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4216    /// those record a depth, so a group containing one is not pushed over the
4217    /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4218    /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4219    fn renders_without_expanding(&self, v: &Value) -> bool {
4220        let plain_props = |p: &IndexMap<String, Value>| {
4221            p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4222        };
4223        match self.get(v) {
4224            // A regex never recurses, with or without its hidden `lastIndex`.
4225            Some(JsObj::RegExp(_)) => true,
4226            Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4227            Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4228            Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4229            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4230                Some("Buffer") => inspect_custom(),
4231                // Own properties added to a Date DO get expanded after it.
4232                Some("Date") => plain_props(p),
4233                Some(_) => false,
4234                None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4235            },
4236            _ => false,
4237        }
4238    }
4239
4240    fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4241        if !matches!(v, Value::Obj(_)) {
4242            return self.inspect_value(v, indent, st);
4243        }
4244        if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4245            return format!("[Circular *{}]", st.mark(self, v));
4246        }
4247        st.seen.push(v.clone());
4248        // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4249        // it expands — not a running maximum — so after the children have been
4250        // rendered it holds the depth of the last chain below this group, which
4251        // is what `reduceToSingleString` compares. A value the depth limit
4252        // stubs out as `[Object]` is never expanded and must not count, or an
4253        // object whose deepest level was elided would break where node joins.
4254        // Only a value node actually EXPANDS advances the depth. A string,
4255        // symbol or bigint is a JS primitive that this host happens to store on
4256        // the heap, so it reaches here as `Value::Obj` where an unboxed number
4257        // returns above — and counting it as a level made any group holding one
4258        // look deeper than it was. Under `compact: 1` that is the difference
4259        // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4260        // Map across four lines, because its string KEYS were being charged a
4261        // nesting level.
4262        if indent as i64 <= inspect_indent_limit()
4263            && !is_primitive(self, v)
4264            && !self.renders_without_expanding(v)
4265        {
4266            st.deepest = indent;
4267        }
4268        let body = self.inspect_value(v, indent, st);
4269        st.seen.pop();
4270        match st.id_of(self, v) {
4271            Some(id) => format!("<ref *{id}> {body}"),
4272            None => body,
4273        }
4274    }
4275
4276    /// The rendering itself, once `inspect_lvl` has established that `v` is not
4277    /// a back-edge into an object already on the stack.
4278    fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4279        if let Some(s) = self.inspect_wrapper(v, indent, st) {
4280            return s;
4281        }
4282        match v {
4283            Value::Undef => "undefined".into(),
4284            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4285            Value::Int(n) => n.to_string(),
4286            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4287            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4288            Value::Float(f) => fmt_number(*f),
4289            Value::Str(s) => quote_str(s),
4290            Value::Obj(_) => match self.get(v) {
4291                Some(JsObj::Str(s)) => quote_str(s),
4292                Some(JsObj::Null) => "null".into(),
4293                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4294                Some(JsObj::BigInt(b)) => format!("{b}n"),
4295                // `lastIndex` is a non-enumerable own property of every regex,
4296                // so `showHidden` (and therefore `%o`) appends it:
4297                // `/x/g { [lastIndex]: 0 }`.
4298                Some(JsObj::RegExp(r)) => {
4299                    let body = format!("/{}/{}", r.source, r.flags);
4300                    if inspect_show_hidden() {
4301                        format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4302                    } else {
4303                        body
4304                    }
4305                }
4306                // `util.inspect` on node v26.7.0 renders a proxy as
4307                // `Proxy(<target>)` — the target's own rendering, wrapped. It
4308                // deliberately does NOT run the handler's traps, so this stays a
4309                // pure `&self` read like every other inspect arm.
4310                Some(JsObj::Proxy { target, .. }) => {
4311                    format!("Proxy({})", self.inspect_lvl(target, indent, st))
4312                }
4313                // `arguments` is backed by an Array but is an ordinary-shaped
4314                // exotic to util.inspect: node prints its indices as quoted
4315                // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4316                Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4317                    if indent as i64 > inspect_indent_limit() {
4318                        return "[Arguments]".into();
4319                    }
4320                    let mut inner: Vec<String> = items
4321                        .iter()
4322                        .enumerate()
4323                        .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4324                        .collect();
4325                    for k in self.fn_prop_keys(v) {
4326                        if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4327                            continue;
4328                        }
4329                        let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4330                        inner.push(format!(
4331                            "{}: {}",
4332                            fmt_key(&k),
4333                            self.inspect_lvl(&val, indent + 2, st)
4334                        ));
4335                    }
4336                    if inner.is_empty() {
4337                        return "[Arguments] {}".into();
4338                    }
4339                    self.render_object(&inner, "[Arguments] ", indent, st)
4340                }
4341                Some(JsObj::Array(items)) => {
4342                    // Own enumerable non-index string props (e.g. a `str.match(re)`
4343                    // result's `index`/`input`/`groups`, or a user-assigned
4344                    // `arr.foo`) render after the elements, as `key: value`.
4345                    let prop_keys: Vec<String> = self
4346                        .fn_prop_keys(v)
4347                        .into_iter()
4348                        .filter(|k| {
4349                            !k.starts_with("@@")
4350                                && !k.starts_with('#')
4351                                && self.prop_attrs(v, k).enumerable
4352                        })
4353                        .collect();
4354                    // An own enumerable SYMBOL-keyed property renders after the
4355                    // string keys as `Symbol(desc): value`, as it does on an
4356                    // object receiver.
4357                    // An instance of an Array SUBCLASS leads with its
4358                    // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4359                    // `getPrefix` does for any non-`Array` constructor.
4360                    let sub = match self.proto_of(v) {
4361                        Some(_) => self.ctor_name(v),
4362                        None => String::new(),
4363                    };
4364                    let base = if sub.is_empty() || sub == "Array" {
4365                        String::new()
4366                    } else {
4367                        format!("{sub}({}) ", items.len())
4368                    };
4369                    let sym_entries = self.own_symbol_entries(v);
4370                    // Under `showHidden` even an empty array has something to
4371                    // show — node prints `[ [length]: 0 ]`, not `[]`.
4372                    if items.is_empty()
4373                        && prop_keys.is_empty()
4374                        && sym_entries.is_empty()
4375                        && !inspect_show_hidden()
4376                    {
4377                        return format!("{base}[]");
4378                    }
4379                    // Node's default inspect depth is 2 (root = depth 0); deeper
4380                    // nesting collapses to `[Array]`. indent grows by 2 per level.
4381                    if indent as i64 > inspect_indent_limit() {
4382                        return "[Array]".into();
4383                    }
4384                    // `util.inspect`'s `maxArrayLength` (default 100): only the
4385                    // first 100 elements are formatted, and the rest collapse to
4386                    // a `... N more items` entry. Without the cap a 120-element
4387                    // array printed all 120 — and, because the grid column width
4388                    // is computed from what is SHOWN, every column was also one
4389                    // character wider than node's.
4390                    // A SPARSE array takes node's `formatSpecialArray` path: an
4391                    // elided run renders as `<N empty items>` rather than as the
4392                    // `undefined` it reads back as.
4393                    let (mut inner, has_tail) = if self.has_holes(v) {
4394                        self.inspect_sparse(v, items, indent, st)
4395                    } else {
4396                        let shown = items.len().min(inspect_max_array_length());
4397                        let mut inner: Vec<String> = items[..shown]
4398                            .iter()
4399                            .map(|x| self.inspect_lvl(x, indent + 2, st))
4400                            .collect();
4401                        let remaining = items.len() - shown;
4402                        if remaining > 0 {
4403                            let unit = if remaining == 1 { "item" } else { "items" };
4404                            inner.push(format!("... {remaining} more {unit}"));
4405                        }
4406                        (inner, remaining > 0)
4407                    };
4408                    // `showHidden` exposes the non-enumerable `length`, which an
4409                    // array always has. It sorts BEFORE any own property node
4410                    // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4411                    // rather than an element, it also turns the column grid off —
4412                    // which is why a ten-element array under `showHidden` prints
4413                    // on one line rather than as a grid.
4414                    let show_hidden = inspect_show_hidden();
4415                    if show_hidden {
4416                        inner.push(format!("[length]: {}", items.len()));
4417                    }
4418                    let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4419                    for k in &prop_keys {
4420                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4421                        inner.push(format!(
4422                            "{}: {}",
4423                            fmt_key(k),
4424                            self.inspect_lvl(&val, indent + 2, st)
4425                        ));
4426                    }
4427                    for (k, val) in &sym_entries {
4428                        let label = match self.symbol_of_key(k) {
4429                            Some(s) => self.inspect(&s),
4430                            None => continue,
4431                        };
4432                        inner.push(format!(
4433                            "{label}: {}",
4434                            self.inspect_lvl(val, indent + 2, st)
4435                        ));
4436                    }
4437                    self.render_array(
4438                        &inner,
4439                        items,
4440                        indent,
4441                        ArrayLayout {
4442                            has_props,
4443                            has_tail,
4444                            base: &base,
4445                        },
4446                        st,
4447                    )
4448                }
4449                // `URLSearchParams` renders its pairs, not its slots:
4450                // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4451                // which is why it is a pair list rather than a Map rendering.
4452                Some(JsObj::Object(props))
4453                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4454                        == Some("URLSearchParams") =>
4455                {
4456                    let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4457                        Some(JsObj::Array(items)) => items.clone(),
4458                        _ => Vec::new(),
4459                    };
4460                    if pairs.is_empty() {
4461                        return "URLSearchParams {}".into();
4462                    }
4463                    let inner: Vec<String> = pairs
4464                        .iter()
4465                        .filter_map(|kv| match self.get(kv) {
4466                            Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4467                                "{} => {}",
4468                                self.inspect_lvl(&p[0], indent + 2, st),
4469                                self.inspect_lvl(&p[1], indent + 2, st)
4470                            )),
4471                            _ => None,
4472                        })
4473                        .collect();
4474                    self.render_object(&inner, "URLSearchParams ", indent, st)
4475                }
4476                // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4477                // constructor and length, then the elements laid out exactly as
4478                // an array's. Without this it fell through to the generic object
4479                // arm and printed the `{ length, byteLength, byteOffset,
4480                // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4481                // which is the whole reason anyone logs one.
4482                Some(JsObj::Object(props))
4483                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4484                        == Some("TypedArray") =>
4485                {
4486                    let kind = props
4487                        .get("@@kind")
4488                        .map(|k| self.str_of(k))
4489                        .unwrap_or_else(|| "TypedArray".into());
4490                    // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4491                    // which this shared borrow cannot allocate as values.
4492                    let elems = crate::stdlib::typedarray::elems_display(self, v);
4493                    // The grid layout sizes its columns from the VALUES; a
4494                    // 64-bit view's come back as `undefined` (no allocation is
4495                    // possible here), which only affects column padding.
4496                    let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4497                    let base = format!("{kind}({}) ", elems.len());
4498                    if indent as i64 > inspect_indent_limit() {
4499                        return format!("[{kind}]");
4500                    }
4501                    let shown = elems.len().min(inspect_max_array_length());
4502                    let mut inner: Vec<String> = elems[..shown].to_vec();
4503                    let remaining = elems.len() - shown;
4504                    if remaining > 0 {
4505                        let unit = if remaining == 1 { "item" } else { "items" };
4506                        inner.push(format!("... {remaining} more {unit}"));
4507                    }
4508                    // A view's whole identity — its element width, its window
4509                    // onto the backing store, and the store itself — is
4510                    // non-enumerable, so `showHidden` is the only way to see it.
4511                    // `util.format('%o', view)` goes through here, since `%o`
4512                    // implies `showHidden`.
4513                    let show_hidden = inspect_show_hidden();
4514                    if show_hidden {
4515                        let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4516                        let byte_offset = props
4517                            .get("byteOffset")
4518                            .map(|x| self.to_number(x))
4519                            .unwrap_or(0.0);
4520                        inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4521                        inner.push(format!("[length]: {}", elems.len()));
4522                        inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4523                        inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4524                        // An ArrayBuffer reached AS a view's backing store is
4525                        // rendered by node WITHOUT its contents — just
4526                        // `ArrayBuffer { [byteLength]: N }` — even though the
4527                        // same buffer inspected on its own leads with
4528                        // `[Uint8Contents]`. Recursing through the normal
4529                        // ArrayBuffer branch therefore printed the bytes twice,
4530                        // once as the view's elements and again as the store's.
4531                        let buf_len = props
4532                            .get("@@buffer")
4533                            .and_then(|b| self.get(b))
4534                            .and_then(|o| match o {
4535                                JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4536                                _ => None,
4537                            })
4538                            .and_then(|b| {
4539                                self.get(&b).map(|o| match o {
4540                                    JsObj::Array(items) => items.len(),
4541                                    _ => 0,
4542                                })
4543                            })
4544                            .unwrap_or(0);
4545                        inner.push(format!(
4546                            "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4547                        ));
4548                    }
4549                    self.render_array(
4550                        &inner,
4551                        &vals,
4552                        indent,
4553                        ArrayLayout {
4554                            has_props: show_hidden,
4555                            has_tail: remaining > 0,
4556                            base: &base,
4557                        },
4558                        st,
4559                    )
4560                }
4561                // An `ArrayBuffer` renders its CONTENTS, which is the only way
4562                // to see them — it exposes no indices of its own:
4563                // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4564                Some(JsObj::Object(props))
4565                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4566                        == Some("ArrayBuffer") =>
4567                {
4568                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4569                        Some(JsObj::Array(items)) => {
4570                            items.iter().map(|x| self.to_number(x) as u8).collect()
4571                        }
4572                        _ => Vec::new(),
4573                    };
4574                    let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4575                    let mut parts = vec![
4576                        format!("[Uint8Contents]: <{}>", hex.join(" ")),
4577                        format!("[byteLength]: {}", bytes.len()),
4578                    ];
4579                    if props.contains_key("@@maxByteLength") {
4580                        let max = props
4581                            .get("@@maxByteLength")
4582                            .map(|m| self.to_number(m))
4583                            .unwrap_or(0.0);
4584                        parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4585                    }
4586                    self.render_object(&parts, "ArrayBuffer ", indent, st)
4587                }
4588                // A live Map/Set iterator shows what it has left to yield, as
4589                // node's `formatIterator` does: `[Map Entries] { [ 1, 'a' ] }`,
4590                // `[Set Iterator] { 1 }`, and `{  }` once it is exhausted.
4591                Some(JsObj::Object(_))
4592                    if crate::builtins::collection_iterator_view(self, v).is_some() =>
4593                {
4594                    let (brand, rest) = crate::builtins::collection_iterator_view(self, v)
4595                        .unwrap_or(("Map Iterator", Vec::new()));
4596                    if indent as i64 > inspect_indent_limit() {
4597                        let stub = if brand.starts_with("Map") {
4598                            "Map Iterator"
4599                        } else {
4600                            "Set Iterator"
4601                        };
4602                        return format!("[Object [{stub}]]");
4603                    }
4604                    let entries = brand.ends_with("Entries");
4605                    let shown = rest.len().min(100);
4606                    let mut inner: Vec<String> = rest[..shown]
4607                        .iter()
4608                        .map(|(k, val)| {
4609                            let ks = self.inspect_lvl(k, indent + 2, st);
4610                            if entries {
4611                                let vs = self.inspect_lvl(val, indent + 2, st);
4612                                format!("[ {ks}, {vs} ]")
4613                            } else {
4614                                ks
4615                            }
4616                        })
4617                        .collect();
4618                    if rest.len() > shown {
4619                        let more = rest.len() - shown;
4620                        inner.push(format!(
4621                            "... {more} more item{}",
4622                            if more == 1 { "" } else { "s" }
4623                        ));
4624                    }
4625                    if inner.is_empty() {
4626                        return format!("[{brand}] {{  }}");
4627                    }
4628                    self.render_object(&inner, &format!("[{brand}] "), indent, st)
4629                }
4630                // A `Date` renders as its ISO-8601 form. Its time value lives in
4631                // the internal `@@ms` slot, which the generic object branch below
4632                // does not show, so without this arm every Date printed as `{}` —
4633                // including through `console.log(d)`, inside arrays, objects and
4634                // Maps, and in an `assert` diff.
4635                Some(JsObj::Object(props))
4636                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4637                {
4638                    let base = crate::stdlib::date::inspect_with_host(self, v);
4639                    // Own properties added to a Date follow the date itself, the
4640                    // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4641                    let extra = self.side_table_parts(v, indent, st);
4642                    let mut inner: Vec<String> = props
4643                        .iter()
4644                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4645                        .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4646                        .collect();
4647                    inner.extend(extra);
4648                    if inner.is_empty() {
4649                        return base;
4650                    }
4651                    self.render_object(&inner, &format!("{base} "), indent, st)
4652                }
4653                // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4654                // at 50 with a `... N more byte(s)` tail, exactly as
4655                // `util.inspect` does. Without this a `console.log(buf)` (the
4656                // single most common thing anyone does with a Buffer) printed
4657                // the internal `{ length, byteLength, … }` bookkeeping.
4658                Some(JsObj::Object(props))
4659                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4660                        == Some("Buffer") =>
4661                {
4662                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4663                        Some(JsObj::Array(items)) => {
4664                            items.iter().map(|x| self.to_number(x) as u8).collect()
4665                        }
4666                        _ => Vec::new(),
4667                    };
4668                    // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4669                    // the shape of the object. Under `customInspect: false` node
4670                    // does not call that hook and falls back to the generic
4671                    // byte-view rendering — which is what an `assert` diff shows,
4672                    // since assert inspects with the hook disabled so that a
4673                    // failure names the differing BYTE rather than two opaque hex
4674                    // blobs. The constructor is `Buffer` while the brand is still
4675                    // `Uint8Array`, so node prints both.
4676                    if !inspect_custom() {
4677                        let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4678                        if indent as i64 > inspect_indent_limit() {
4679                            return "[Buffer [Uint8Array]]".into();
4680                        }
4681                        let shown = bytes.len().min(inspect_max_array_length());
4682                        let mut inner: Vec<String> =
4683                            bytes[..shown].iter().map(|b| b.to_string()).collect();
4684                        let vals: Vec<Value> = bytes[..shown]
4685                            .iter()
4686                            .map(|b| Value::Float(*b as f64))
4687                            .collect();
4688                        let remaining = bytes.len() - shown;
4689                        if remaining > 0 {
4690                            let unit = if remaining == 1 { "item" } else { "items" };
4691                            inner.push(format!("... {remaining} more {unit}"));
4692                        }
4693                        return self.render_array(
4694                            &inner,
4695                            &vals,
4696                            indent,
4697                            ArrayLayout {
4698                                has_props: false,
4699                                has_tail: remaining > 0,
4700                                base: &base,
4701                            },
4702                            st,
4703                        );
4704                    }
4705                    const MAX: usize = 50;
4706                    let shown: Vec<String> =
4707                        bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4708                    let mut out = format!("<Buffer {}", shown.join(" "));
4709                    if bytes.len() > MAX {
4710                        let more = bytes.len() - MAX;
4711                        let unit = if more == 1 { "byte" } else { "bytes" };
4712                        out.push_str(&format!(" ... {more} more {unit}"));
4713                    }
4714                    out.push('>');
4715                    out
4716                }
4717                // An Error inspects as its `.stack` — never as an object literal
4718                // exposing the internal `message`/`stack` slots. Any own property
4719                // a script added beyond those follows in braces, as V8 renders
4720                // it: `Error: x\n    at … { code: 'C' }`.
4721                Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4722                    let mut stack = lookup_chain(self, v, "stack")
4723                        .map(|s| self.str_of(&s))
4724                        .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4725                    // A `DOMException` prints its CLASS and then its name —
4726                    // `DOMException [AbortError]: m` — where a plain error
4727                    // prints only its stack head.
4728                    if let Some(JsObj::Object(p)) = self.get(v) {
4729                        if let Some(n) = p.get("@@domName") {
4730                            let name = self.str_of(n);
4731                            stack = format!(
4732                                "DOMException [{name}]{}",
4733                                stack.strip_prefix(&name).unwrap_or(&stack)
4734                            );
4735                        }
4736                    }
4737                    let extra: Vec<String> = self
4738                        .own_enum_key_names(v)
4739                        .into_iter()
4740                        .filter(|k| k != "name")
4741                        .map(|k| {
4742                            let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4743                                Some(JsObj::Object(p)) => {
4744                                    p.get(&k).cloned().unwrap_or(Value::Undef)
4745                                }
4746                                _ => Value::Undef,
4747                            });
4748                            format!(
4749                                "{}: {}",
4750                                fmt_key(&k),
4751                                self.inspect_lvl(&val, indent + 2, st)
4752                            )
4753                        })
4754                        .collect();
4755                    if extra.is_empty() {
4756                        stack
4757                    } else {
4758                        format!("{stack} {{ {} }}", extra.join(", "))
4759                    }
4760                }
4761                Some(JsObj::Object(props)) => {
4762                    // Instances print with their constructor name as a prefix
4763                    // (`C { x: 1 }`); plain objects have none; a null-prototype
4764                    // object (e.g. an `Object.groupBy` result) is tagged
4765                    // `[Object: null prototype]`.
4766                    let ctor = match self.ctor_name(v) {
4767                        n if n.is_empty() => "Object".to_string(),
4768                        n => n,
4769                    };
4770                    let plain_prefix = if ctor == "Object" {
4771                        String::new()
4772                    } else {
4773                        format!("{ctor} ")
4774                    };
4775                    let prefix = if self.inspects_null_proto(v) {
4776                        "[Object: null prototype] ".to_string()
4777                    } else {
4778                        // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4779                        match self.inspect_tag(v) {
4780                            Some(t) if t != ctor => format!("{ctor} [{t}] "),
4781                            _ => plain_prefix.clone(),
4782                        }
4783                    };
4784                    // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4785                    // private class fields; a real symbol-keyed own property is a
4786                    // visible one and renders as `Symbol(desc): value`.
4787                    // An own ACCESSOR has no value to print: node shows the
4788                    // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4789                    // place. It is found through the `@@ord:` marker the
4790                    // property map holds for it, which is also what puts it in
4791                    // declaration order among the data properties. Without this
4792                    // an accessor rendered as nothing at all — `{ get z(){} }`
4793                    // printed `{}`.
4794                    let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4795                        .iter()
4796                        .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4797                            Some(real) => {
4798                                let attrs = self.prop_attrs(v, real);
4799                                let label = match self.own_accessor(v, real)? {
4800                                    (Some(_), Some(_)) => "[Getter/Setter]",
4801                                    (Some(_), None) => "[Getter]",
4802                                    (None, Some(_)) => "[Setter]",
4803                                    (None, None) => return None,
4804                                };
4805                                attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4806                            }
4807                            // Only an ENUMERABLE own property is shown, as node
4808                            // does: a native instance keeps bookkeeping (a
4809                            // `URLSearchParams`'s `size`) as a hidden own slot,
4810                            // and printing it would report a spec getter as data.
4811                            None if !k.starts_with("@@")
4812                                && !k.starts_with('#')
4813                                && self.prop_attrs(v, k).enumerable =>
4814                            {
4815                                Some((fmt_key(k), Ok(val)))
4816                            }
4817                            None => None,
4818                        })
4819                        .collect();
4820                    shown.extend(props.iter().filter_map(|(k, val)| {
4821                        let sym = self.symbol_of_key(k)?;
4822                        self.prop_attrs(v, k)
4823                            .enumerable
4824                            .then(|| (self.inspect(&sym), Ok(val)))
4825                    }));
4826                    if shown.is_empty() {
4827                        return format!("{prefix}{{}}");
4828                    }
4829                    // Depth limit (Node default 2): deeper objects collapse to
4830                    // `[Object]` (or `[ClassName]` for a named instance).
4831                    if indent as i64 > inspect_indent_limit() {
4832                        return if self.inspects_null_proto(v) {
4833                            // Already bracketed (`[Object: null prototype]`).
4834                            prefix.trim_end().to_string()
4835                        } else if plain_prefix.is_empty() {
4836                            "[Object]".into()
4837                        } else {
4838                            format!("[{}]", plain_prefix.trim_end())
4839                        };
4840                    }
4841                    let inner: Vec<String> = shown
4842                        .iter()
4843                        .map(|(k, val)| match val {
4844                            Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4845                            Err(label) => format!("{k}: {label}"),
4846                        })
4847                        .collect();
4848                    self.render_object(&inner, &prefix, indent, st)
4849                }
4850                Some(JsObj::Symbol { desc, .. }) => match desc {
4851                    Some(d) => format!("Symbol({d})"),
4852                    None => "Symbol()".into(),
4853                },
4854                Some(JsObj::Class(c)) => {
4855                    // Named like a function (`callable_name`), so a class
4856                    // expression picks up its inferred binding name, and one
4857                    // with no name at all prints `(anonymous)`. node's
4858                    // `getClassBase` appends `extends <name>` only when the
4859                    // parent HAS a name: `class extends null {}` prints
4860                    // without it.
4861                    let mut name = self.callable_name(v);
4862                    if name.is_empty() {
4863                        name = "(anonymous)".into();
4864                    }
4865                    let pname = c
4866                        .parent
4867                        .as_ref()
4868                        .map(|p| self.callable_name(p))
4869                        .unwrap_or_default();
4870                    let base = if pname.is_empty() {
4871                        format!("[class {name}]")
4872                    } else {
4873                        format!("[class {name} extends {pname}]")
4874                    };
4875                    self.with_callable_props(v, base, indent, st)
4876                }
4877                // A Map/Set renders its members at the NEXT nesting level, and
4878                // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4879                // array collapses to `[Array]`. Both used to recurse through
4880                // `inspect`, which restarts at indent 0, so the depth gate never
4881                // fired: nesting printed one level too deep at every depth
4882                // (measured on node v26.7.0, four nested Maps print
4883                // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4884                // and a SELF-referential Map or Set recursed forever and aborted
4885                // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4886                // died with `fatal runtime error: stack overflow`, which no
4887                // `try`/`catch` can see. An empty one still prints in full at any
4888                // depth, as `[]`/`{}` do.
4889                // A WEAK collection never shows its contents: node prints
4890                // `WeakMap { <items unknown> }` whether it holds anything or
4891                // not, because the entries are not enumerable by design.
4892                Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4893                Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4894                Some(JsObj::Map { entries, .. }) => {
4895                    let extra = self.side_table_parts(v, indent, st);
4896                    let prefix = self.builtin_prefix(v, "Map", Some(entries.len()));
4897                    if entries.is_empty() && extra.is_empty() {
4898                        return format!("{prefix}{{}}");
4899                    }
4900                    if indent as i64 > inspect_indent_limit() {
4901                        return self.builtin_depth_stub(v, "Map");
4902                    }
4903                    let mut inner: Vec<String> = entries
4904                        .values()
4905                        .map(|(k, val)| {
4906                            // Sequenced, not nested in one `format!`: both arms
4907                            // need the same `&mut` cycle state.
4908                            let ks = self.inspect_lvl(k, indent + 2, st);
4909                            let vs = self.inspect_lvl(val, indent + 2, st);
4910                            format!("{ks} => {vs}")
4911                        })
4912                        .collect();
4913                    inner.extend(extra);
4914                    // Laid out by the SAME routine as a plain object, not joined
4915                    // onto one line unconditionally. `Map`/`Set` were the only
4916                    // containers that never consulted `breakLength` or `compact`,
4917                    // so every collection wide enough to wrap printed as one long
4918                    // line: node breaks a seven-member Set of ten-character
4919                    // strings across seven lines, and `util.inspect(m, {compact:
4920                    // false})` — which assert's own diff renderer depends on —
4921                    // could not break a Map at all. Node builds these through
4922                    // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4923                    // is this `prefix` (the trailing space is the brace gap).
4924                    self.render_object(&inner, &prefix, indent, st)
4925                }
4926                Some(JsObj::Set { entries, .. }) => {
4927                    let extra = self.side_table_parts(v, indent, st);
4928                    let prefix = self.builtin_prefix(v, "Set", Some(entries.len()));
4929                    if entries.is_empty() && extra.is_empty() {
4930                        return format!("{prefix}{{}}");
4931                    }
4932                    if indent as i64 > inspect_indent_limit() {
4933                        return self.builtin_depth_stub(v, "Set");
4934                    }
4935                    let mut inner: Vec<String> = entries
4936                        .values()
4937                        .map(|v| self.inspect_lvl(v, indent + 2, st))
4938                        .collect();
4939                    inner.extend(extra);
4940                    // Same layout routine as a Map (see above). Note node does
4941                    // NOT column-group a wide Set the way it grids an array:
4942                    // `groupArrayElements` is reached only from the list
4943                    // formatter, so a 30-member Set is thirty lines.
4944                    self.render_object(&inner, &prefix, indent, st)
4945                }
4946                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4947                Some(JsObj::Iter { array: Some(_), .. }) => "Object [Array Iterator] {}".into(),
4948                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4949                    Some(c) => {
4950                        // `P2 [Promise] { 3 }` for an instance of a subclass.
4951                        let prefix = self.builtin_prefix(v, "Promise", None);
4952                        match c.state {
4953                            PromiseState::Pending => format!("{prefix}{{ <pending> }}"),
4954                            PromiseState::Fulfilled => {
4955                                format!("{prefix}{{ {} }}", self.inspect_lvl(&c.value, 0, st))
4956                            }
4957                            PromiseState::Rejected => format!(
4958                                "{prefix}{{ <rejected> {} }}",
4959                                self.inspect_lvl(&c.value, 0, st)
4960                            ),
4961                        }
4962                    }
4963                    None => "Promise { <pending> }".into(),
4964                },
4965                Some(JsObj::Func(f)) => {
4966                    // `callable_name`, not the FuncDef name: an anonymous
4967                    // function expression gets its name by inference from the
4968                    // binding it initialises (`const f = function(){}`), and
4969                    // that lands as an own `name` property.
4970                    let name = self.callable_name(v);
4971                    // util.inspect labels a function by its kind, the same
4972                    // string V8 gives it as `Symbol.toStringTag`:
4973                    // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4974                    let kind = match self.funcs.get(f.def_id) {
4975                        Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4976                        Some(d) if d.is_generator => "GeneratorFunction",
4977                        Some(d) if d.is_async => "AsyncFunction",
4978                        _ => "Function",
4979                    };
4980                    let base = if name.is_empty() {
4981                        format!("[{kind} (anonymous)]")
4982                    } else {
4983                        format!("[{kind}: {name}]")
4984                    };
4985                    self.with_callable_props(v, base, indent, st)
4986                }
4987                Some(JsObj::Builtin(n)) => {
4988                    // A namespace object is not a function and must not be
4989                    // printed as one. The three ECMAScript namespaces carry a
4990                    // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4991                    // their members are all non-enumerable, so the braces really
4992                    // are empty. A `require()`d module namespace has no tag and
4993                    // node prints its members, which cannot be rendered here —
4994                    // formatting a member means allocating its value, and this
4995                    // runs under the host borrow.
4996                    if !builtin_is_callable(n) {
4997                        match crate::builtins::well_known_tag(self, v) {
4998                            Some(tag) => format!("Object [{tag}] {{}}"),
4999                            // `Set.prototype` inspects under the CONSTRUCTOR's
5000                            // name, not the key: node prints `Object [Set] {}`.
5001                            None => {
5002                                format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
5003                            }
5004                        }
5005                    } else {
5006                        format!("[Function: {}]", crate::builtins::builtin_name(n))
5007                    }
5008                }
5009                // A bound method is not anonymous: it is the prototype method it
5010                // resolves to, so `console.log(new Uint8Array(1).set)` reports
5011                // `[Function: set]`.
5012                Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
5013                Some(JsObj::BoundFunc { target, .. }) => {
5014                    let n = self.callable_name(target);
5015                    if n.is_empty() {
5016                        "[Function: bound ]".into()
5017                    } else {
5018                        format!("[Function: bound {n}]")
5019                    }
5020                }
5021                _ => "undefined".into(),
5022            },
5023            _ => "undefined".into(),
5024        }
5025    }
5026
5027    /// Append a callable's own enumerable properties to its `[Function: f]` /
5028    /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
5029    /// A callable with none renders as the bare base.
5030    fn with_callable_props(
5031        &self,
5032        v: &Value,
5033        base: String,
5034        indent: usize,
5035        st: &mut InspectCycles,
5036    ) -> String {
5037        let mut inner: Vec<String> = self
5038            .own_enum_key_names(v)
5039            .into_iter()
5040            .map(|k| {
5041                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
5042                format!(
5043                    "{}: {}",
5044                    fmt_key(&k),
5045                    self.inspect_lvl(&val, indent + 2, st)
5046                )
5047            })
5048            .collect();
5049        for (k, val) in self.own_symbol_entries(v) {
5050            if let Some(sym) = self.symbol_of_key(&k) {
5051                inner.push(format!(
5052                    "{}: {}",
5053                    self.inspect(&sym),
5054                    self.inspect_lvl(&val, indent + 2, st)
5055                ));
5056            }
5057        }
5058        if inner.is_empty() {
5059            return base;
5060        }
5061        self.render_object(&inner, &format!("{base} "), indent, st)
5062    }
5063
5064    /// Render a non-empty array's already-formatted element strings, applying
5065    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
5066    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
5067    /// `values` is the raw element list (drives numeric right-alignment); `indent`
5068    /// is the array's own indentation level.
5069    fn render_array(
5070        &self,
5071        output: &[String],
5072        values: &[Value],
5073        indent: usize,
5074        opts: ArrayLayout<'_>,
5075        st: &InspectCycles,
5076    ) -> String {
5077        let ArrayLayout {
5078            has_props,
5079            has_tail,
5080            base,
5081        } = opts;
5082        // Group array elements together if the array has more than six entries.
5083        // Arrays carrying extra own props (`index`/`input`/… on a match result)
5084        // are never grid-grouped — Node lays those out plainly.
5085        // `compact: false` (held as 0) also turns the GRID off, not just the
5086        // single-line join. Node reaches `groupArrayElements` only under
5087        // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
5088        // element per line however many there are; without this gate a 30-element
5089        // array still came back column-aligned in three rows, which is the form
5090        // assert's diff renderer splits on — every array diff would have been
5091        // computed over grid rows instead of elements.
5092        let entries = output.len();
5093        let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
5094            group_array_elements(self, output, values, indent, has_tail)
5095        } else {
5096            (output.to_vec(), false)
5097        };
5098        // A typed array prints its constructor and length ahead of the brackets
5099        // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
5100        // break-length seed, so a long tag wraps the list one entry sooner.
5101        if output.is_empty() {
5102            return format!("{base}[]");
5103        }
5104        // If no grouping happened, try to line everything up on a single line.
5105        if !grouped {
5106            // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5107            let start = output.len() + indent + 1 + base.chars().count() + 10;
5108            if self.may_compact(indent, st) && is_below_break_length(output, start) {
5109                return format!("{base}[ {} ]", output.join(", "));
5110            }
5111        }
5112        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5113        let pad = " ".repeat(indent);
5114        let sep = format!(",\n{pad}  ");
5115        format!("{base}[\n{pad}  {}\n{pad}]", lines.join(&sep))
5116    }
5117
5118    /// Render a non-empty object's already-formatted `key: value` strings with
5119    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5120    /// else one property per line indented by `indent + 2`. `prefix` is the
5121    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5122    /// Mirrors `render_array`'s break decision, including the `compact` depth
5123    /// gate.
5124    /// Whether a group at `indent` may be joined onto one line.
5125    ///
5126    /// Node's `reduceToSingleString`: only while the subtree below this group is
5127    /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5128    /// nothing qualifies and every group breaks.
5129    fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5130        let compact = inspect_compact();
5131        if compact < 1 {
5132            return false;
5133        }
5134        // Levels, not columns: the indent advances by two per level.
5135        let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5136        (depth_below as i64) < compact
5137    }
5138
5139    fn render_object(
5140        &self,
5141        output: &[String],
5142        prefix: &str,
5143        indent: usize,
5144        st: &InspectCycles,
5145    ) -> String {
5146        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5147        // For a tagged object Node folds the tag into `braces[0]` (e.g.
5148        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5149        // prefix (which carries the trailing space) plus the `{`.
5150        // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5151        // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5152        // the object shape), which is observably different from sorting keys
5153        // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5154        // `assert`'s diff renderer depends on this: without it two objects
5155        // carrying the same properties in a different insertion order diffed as
5156        // a wholesale rewrite of every line instead of as equal.
5157        let sorted_output;
5158        let output = if inspect_sorted() {
5159            let mut v = output.to_vec();
5160            v.sort();
5161            sorted_output = v;
5162            &sorted_output[..]
5163        } else {
5164            output
5165        };
5166        let braces0 = prefix.chars().count() + 1;
5167        let start = output.len() + indent + braces0 + 10;
5168        if self.may_compact(indent, st) && is_below_break_length(output, start) {
5169            return format!("{prefix}{{ {} }}", output.join(", "));
5170        }
5171        let pad = " ".repeat(indent);
5172        let sep = format!(",\n{pad}  ");
5173        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
5174    }
5175
5176    /// The `.name` of any callable (function/class/builtin/bound).
5177    pub fn callable_name(&self, v: &Value) -> String {
5178        // A user-set `.name` own property wins.
5179        if let Some(n) = self.fn_prop(v, "name") {
5180            return self.str_of(&n);
5181        }
5182        match self.get(v) {
5183            Some(JsObj::Func(f)) => self
5184                .funcs
5185                .get(f.def_id)
5186                .map(|d| d.name.clone())
5187                .unwrap_or_default(),
5188            Some(JsObj::Class(c)) => c.name.clone(),
5189            // Not the whole key: a builtin's `.name` is its last segment, and a
5190            // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5191            // `.` at all, so this reported the internal spelling verbatim and
5192            // `console.log(Uint8Array.prototype.set)` printed
5193            // `[Function: @proto:TypedArray:set]`.
5194            Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5195            Some(JsObj::BoundFunc { target, .. }) => {
5196                format!("bound {}", self.callable_name(target))
5197            }
5198            Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5199            _ => String::new(),
5200        }
5201    }
5202
5203    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5204
5205    /// Strict equality (`===`): same type and same value, no coercion.
5206    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5207        match (a, b) {
5208            (Value::Undef, Value::Undef) => true,
5209            (Value::Bool(x), Value::Bool(y)) => x == y,
5210            (Value::Str(x), Value::Str(y)) => x == y,
5211            _ => {
5212                // Numbers (NaN !== NaN, +0 === -0).
5213                let an = matches!(a, Value::Int(_) | Value::Float(_));
5214                let bn = matches!(b, Value::Int(_) | Value::Float(_));
5215                if an && bn {
5216                    let x = self.to_number(a);
5217                    let y = self.to_number(b);
5218                    return x == y;
5219                }
5220                // BigInt === BigInt compares by value (each literal is a distinct
5221                // heap cell, so reference identity would be wrong). BigInt is never
5222                // `===` a Number (different types).
5223                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5224                    return x == y;
5225                }
5226                // Heap values.
5227                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5228                    return sa == sb;
5229                }
5230                let na = self.is_null(a);
5231                let nb = self.is_null(b);
5232                if na || nb {
5233                    return na && nb;
5234                }
5235                // A builtin namespace/constructor/prototype is a SINGLETON in JS
5236                // (`Math === Math`, `Array.prototype === Array.prototype`), but
5237                // every bare reference here allocates a fresh handle, so compare
5238                // those by name rather than by heap index.
5239                if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5240                    (self.get(a), self.get(b))
5241                {
5242                    return builtin_identity(x) == builtin_identity(y);
5243                }
5244                // Reference identity for arrays/objects/functions.
5245                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5246            }
5247        }
5248    }
5249
5250    /// Whether `v` is `null` or `undefined`.
5251    pub fn is_nullish(&self, v: &Value) -> bool {
5252        matches!(v, Value::Undef) || self.is_null(v)
5253    }
5254
5255    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5256    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5257    /// `"null"`, or `"object"` (array / plain object / function).
5258    fn js_type(&self, v: &Value) -> &'static str {
5259        match v {
5260            Value::Undef => "undefined",
5261            Value::Bool(_) => "boolean",
5262            Value::Int(_) | Value::Float(_) => "number",
5263            Value::Str(_) => "string",
5264            Value::Obj(_) => match self.get(v) {
5265                Some(JsObj::Str(_)) => "string",
5266                Some(JsObj::Null) => "null",
5267                Some(JsObj::BigInt(_)) => "bigint",
5268                _ => "object",
5269            },
5270            _ => "object",
5271        }
5272    }
5273
5274    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5275    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5276    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5277    /// `[0] == ""` is `false` — never a number coercion of the object.
5278    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5279        // Same type: identical to `===` (number==number, string==string, etc.).
5280        if self.strict_eq(a, b) {
5281            return true;
5282        }
5283        let ta = self.js_type(a);
5284        let tb = self.js_type(b);
5285        // null and undefined are loosely equal only to each other.
5286        if self.is_nullish(a) || self.is_nullish(b) {
5287            return self.is_nullish(a) && self.is_nullish(b);
5288        }
5289        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5290        // values (both-BigInt was already settled by the `strict_eq` above).
5291        if ta == "bigint" || tb == "bigint" {
5292            return self.bigint_loose_eq(a, b);
5293        }
5294        if ta == tb {
5295            // Same type but not strict-equal (and not nullish) ⇒ not equal.
5296            return false;
5297        }
5298        // number ⇄ string: compare as numbers.
5299        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5300            return self.to_number(a) == self.to_number(b);
5301        }
5302        // boolean side coerces to number, then recompares.
5303        if ta == "boolean" {
5304            return self.loose_eq(&Value::Float(self.to_number(a)), b);
5305        }
5306        if tb == "boolean" {
5307            return self.loose_eq(a, &Value::Float(self.to_number(b)));
5308        }
5309        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5310        // then recompare as string==string or number==string.
5311        if ta == "object" && (tb == "number" || tb == "string") {
5312            let pa = self.str_of(a);
5313            return if tb == "string" {
5314                pa == self.str_of(b)
5315            } else {
5316                str_to_number(&pa) == self.to_number(b)
5317            };
5318        }
5319        if tb == "object" && (ta == "number" || ta == "string") {
5320            let pb = self.str_of(b);
5321            return if ta == "string" {
5322                self.str_of(a) == pb
5323            } else {
5324                self.to_number(a) == str_to_number(&pb)
5325            };
5326        }
5327        false
5328    }
5329
5330    /// The numeric-hook arithmetic/relational fallback for non-native operands
5331    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5332    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5333        use NumOp::*;
5334        match op {
5335            Add => {
5336                // `+`: if either operand is a string, concatenate string forms;
5337                // otherwise numeric addition.
5338                let a_str = self.prefers_string(a);
5339                let b_str = self.prefers_string(b);
5340                if a_str || b_str {
5341                    // String concatenation wins even with a bigint operand
5342                    // (`1n + "x"` → `"1x"`).
5343                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
5344                    Ok(self.new_str(s))
5345                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5346                    self.bigint_arith(op, a, b)
5347                } else {
5348                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5349                }
5350            }
5351            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5352                self.bigint_arith(op, a, b)
5353            }
5354            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5355            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5356            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5357            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5358            Pow => Ok(Value::Float(crate::builtins::js_pow(
5359                self.to_number(a),
5360                self.to_number(b),
5361            ))),
5362            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5363            Neg => Ok(Value::Float(-self.to_number(a))),
5364            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5365            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5366            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5367        }
5368    }
5369
5370    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5371    /// which drives `+` toward concatenation. Primitive strings qualify, and so
5372    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5373    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5374    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5375    fn prefers_string(&self, v: &Value) -> bool {
5376        match v {
5377            Value::Str(_) => true,
5378            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5379            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5380            // string primitive either.
5381            Value::Obj(_) => !matches!(
5382                self.get(v),
5383                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5384            ),
5385            _ => false,
5386        }
5387    }
5388
5389    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5390    /// lexicographic, otherwise numeric (NaN yields false).
5391    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5392        use std::cmp::Ordering;
5393        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5394            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5395            x.cmp(&y)
5396        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5397            // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5398            // order once an astral character meets a BMP one — see `utf16`.
5399            crate::utf16::cmp_units(&x, &y)
5400        } else {
5401            let x = self.to_number(a);
5402            let y = self.to_number(b);
5403            match x.partial_cmp(&y) {
5404                Some(o) => o,
5405                None => return false, // NaN operand
5406            }
5407        };
5408        match op {
5409            NumOp::Lt => ord == Ordering::Less,
5410            NumOp::Le => ord != Ordering::Greater,
5411            NumOp::Gt => ord == Ordering::Greater,
5412            NumOp::Ge => ord != Ordering::Less,
5413            _ => false,
5414        }
5415    }
5416
5417    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5418    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5419    /// BigInt with a Number throws, matching Node).
5420    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5421        if self.is_bigint_val(a) || self.is_bigint_val(b) {
5422            return self.bigint_bitwise(tag, a, b);
5423        }
5424        let x = to_int32(self.to_number(a));
5425        let y = to_int32(self.to_number(b));
5426        let r: i64 = match tag {
5427            binop::BITAND => (x & y) as i64,
5428            binop::BITOR => (x | y) as i64,
5429            binop::BITXOR => (x ^ y) as i64,
5430            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5431            binop::SHR => (x >> ((y as u32) & 31)) as i64,
5432            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5433            _ => 0,
5434        };
5435        Ok(Value::Float(r as f64))
5436    }
5437
5438    // ── BigInt operations ────────────────────────────────────────────────────
5439    /// Whether `v` is a heap `BigInt`.
5440    pub fn is_bigint_val(&self, v: &Value) -> bool {
5441        matches!(self.get(v), Some(JsObj::BigInt(_)))
5442    }
5443    /// The `BigInt` value of `v` (a heap bigint), else `None`.
5444    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5445        match self.get(v) {
5446            Some(JsObj::BigInt(b)) => Some(b.clone()),
5447            _ => None,
5448        }
5449    }
5450    /// Allocate a heap `BigInt`.
5451    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5452        self.alloc(JsObj::BigInt(b))
5453    }
5454
5455    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5456    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5457    /// `TypeError` (a string operand is handled as concatenation before we get
5458    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5459    /// exponent.
5460    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5461        use num_traits::{Signed, Zero};
5462        use NumOp::*;
5463        if op == Neg {
5464            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5465            return Ok(self.new_bigint(-x));
5466        }
5467        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5468            (Some(x), Some(y)) => (x, y),
5469            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5470            _ => {
5471                return Err(type_error(
5472                    "Cannot mix BigInt and other types, use explicit conversions",
5473                ))
5474            }
5475        };
5476        let r = match op {
5477            Add => x + y,
5478            Sub => x - y,
5479            Mul => x * y,
5480            Div => {
5481                if y.is_zero() {
5482                    return Err("RangeError: Division by zero".into());
5483                }
5484                x / y // truncates toward zero (matches JS BigInt division)
5485            }
5486            Mod => {
5487                if y.is_zero() {
5488                    return Err("RangeError: Division by zero".into());
5489                }
5490                x % y // sign follows the dividend (truncated), like JS
5491            }
5492            Pow => {
5493                if y.is_negative() {
5494                    return Err("RangeError: Exponent must be positive".into());
5495                }
5496                let exp = num_traits::ToPrimitive::to_u32(&y)
5497                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5498                num_traits::Pow::pow(x, exp)
5499            }
5500            _ => return Err(type_error("unsupported BigInt operation")),
5501        };
5502        Ok(self.new_bigint(r))
5503    }
5504
5505    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5506    /// be BigInt (mixing throws).
5507    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5508        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5509            (Some(x), Some(y)) => (x, y),
5510            _ => {
5511                return Err(type_error(
5512                    "Cannot mix BigInt and other types, use explicit conversions",
5513                ))
5514            }
5515        };
5516        let r = match tag {
5517            binop::BITAND => x & y,
5518            binop::BITOR => x | y,
5519            binop::BITXOR => x ^ y,
5520            binop::SHL => {
5521                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5522                if n >= 0 {
5523                    x << (n as usize)
5524                } else {
5525                    x >> ((-n) as usize)
5526                }
5527            }
5528            binop::SHR => {
5529                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5530                if n >= 0 {
5531                    x >> (n as usize)
5532                } else {
5533                    x << ((-n) as usize)
5534                }
5535            }
5536            binop::USHR => {
5537                return Err(type_error(
5538                    "BigInts have no unsigned right shift, use >> instead",
5539                ))
5540            }
5541            _ => return Err(type_error("unsupported BigInt operation")),
5542        };
5543        Ok(self.new_bigint(r))
5544    }
5545
5546    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5547    /// being BigInt was already handled by `strict_eq`.
5548    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5549        // Order so `big` is the BigInt side and `other` the counterpart.
5550        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5551            (Some(x), _) => (x, b),
5552            (_, Some(y)) => (y, a),
5553            _ => return false,
5554        };
5555        match other {
5556            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5557            Value::Int(n) => big == num_bigint::BigInt::from(*n),
5558            Value::Float(f) => {
5559                // Equal only when the float is an integer with the same value.
5560                if !f.is_finite() || f.fract() != 0.0 {
5561                    return false;
5562                }
5563                bigint_to_f64(&big) == *f
5564            }
5565            Value::Str(s) => match parse_bigint_str(s) {
5566                Some(bs) => big == bs,
5567                None => false,
5568            },
5569            Value::Obj(_) => match self.get(other) {
5570                // A heap string parses like a primitive string.
5571                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5572                _ => {
5573                    // Other objects reduce via ToPrimitive (their string form).
5574                    let s = self.str_of(other);
5575                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5576                }
5577            },
5578            _ => false,
5579        }
5580    }
5581}
5582
5583/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5584/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5585pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5586    let t = crate::utf16::js_trim(s);
5587    if t.is_empty() {
5588        return Some(num_bigint::BigInt::from(0));
5589    }
5590    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5591        (16, h)
5592    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5593        (8, o)
5594    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5595        (2, bb)
5596    } else {
5597        (10, t)
5598    };
5599    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5600}
5601
5602/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5603/// out-of-range magnitudes become ±Infinity, matching Node.
5604pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5605    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5606        if num_traits::Signed::is_negative(b) {
5607            f64::NEG_INFINITY
5608        } else {
5609            f64::INFINITY
5610        }
5611    })
5612}
5613
5614/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5615fn js_mod(a: f64, b: f64) -> f64 {
5616    a % b
5617}
5618
5619/// Cycle bookkeeping for one `util.inspect` render.
5620///
5621/// `seen` is the chain of objects currently being rendered (an entry appearing
5622/// twice is a back-edge), and `refs` records every object a back-edge pointed
5623/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5624/// in `[Circular *N]` / `<ref *N>`.
5625/// How an array-shaped group is laid out, beyond its entries themselves.
5626#[derive(Clone, Copy)]
5627struct ArrayLayout<'a> {
5628    /// Extra own properties follow the elements, which suppresses grid grouping.
5629    has_props: bool,
5630    /// `output`'s last entry is the `... N more items` tail rather than a real
5631    /// element, so the grid must not size a column to it.
5632    has_tail: bool,
5633    /// A constructor tag printed before the brackets, with a trailing space
5634    /// (`"Uint8Array(3) "`), or empty for a plain array.
5635    base: &'a str,
5636}
5637
5638#[derive(Default)]
5639struct InspectCycles {
5640    seen: Vec<Value>,
5641    refs: Vec<Value>,
5642    /// The indent level of the value most recently EXPANDED — node's
5643    /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5644    /// while `currentDepth - thisDepth < compact`, so without it a deeply
5645    /// nested object printed on one line where node breaks the outer levels.
5646    deepest: usize,
5647}
5648
5649impl InspectCycles {
5650    /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5651    fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5652        if let Some(id) = self.id_of(h, v) {
5653            return id;
5654        }
5655        self.refs.push(v.clone());
5656        self.refs.len()
5657    }
5658
5659    /// The `*N` id already assigned to `v`, if any.
5660    fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5661        self.refs
5662            .iter()
5663            .position(|p| h.strict_eq(p, v))
5664            .map(|i| i + 1)
5665    }
5666}
5667
5668thread_local! {
5669    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5670    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5671    /// overrides it for one call, `console.log`/`util.format` use the default.
5672    /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5673    /// "already past the limit" — everything collapses to `[Object]` at the top
5674    /// level. Held as `usize` it read as an enormous depth and expanded fully.
5675    static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5676
5677    /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5678    /// group is put on one line only when the subtree below it is shallower
5679    /// than this. `compact: false` is held as 0, which no subtree depth is
5680    /// below, so every group breaks — which is exactly what node does.
5681    static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5682
5683    /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5684    /// itself passes 80.
5685    static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5686
5687    /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5688    /// order instead of insertion order. Off by default. `assert`'s diff renderer
5689    /// turns it on so that two objects built with the same keys in a different
5690    /// order diff as equal rather than as a wholesale rewrite.
5691    static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5692
5693    /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5694    /// the rest collapse into `... N more items`. Node's default is 100;
5695    /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5696    static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5697
5698    /// `util.inspect`'s `customInspect` option: whether a value's own
5699    /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5700    /// it off so a diff shows an object's real structure rather than whatever
5701    /// summary it prefers to print.
5702    static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5703
5704    /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5705    /// carries — an array's `length`, a typed array's element width and window
5706    /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5707    static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5708}
5709
5710/// Set the `util.inspect` `showHidden` option for the next render.
5711pub fn set_inspect_show_hidden(s: bool) {
5712    INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5713}
5714
5715pub(crate) fn inspect_show_hidden() -> bool {
5716    INSPECT_SHOW_HIDDEN.with(|x| x.get())
5717}
5718
5719/// Set the `util.inspect` `customInspect` option for the next render.
5720pub fn set_inspect_custom(c: bool) {
5721    INSPECT_CUSTOM.with(|x| x.set(c));
5722}
5723
5724pub(crate) fn inspect_custom() -> bool {
5725    INSPECT_CUSTOM.with(|x| x.get())
5726}
5727
5728/// Set the `util.inspect` `sorted` option for the next render.
5729pub fn set_inspect_sorted(s: bool) {
5730    INSPECT_SORTED.with(|x| x.set(s));
5731}
5732
5733pub(crate) fn inspect_sorted() -> bool {
5734    INSPECT_SORTED.with(|x| x.get())
5735}
5736
5737/// Set the `util.inspect` `maxArrayLength` for the next render.
5738pub fn set_inspect_max_array_length(n: usize) {
5739    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5740}
5741
5742pub(crate) fn inspect_max_array_length() -> usize {
5743    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5744}
5745
5746/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5747pub fn set_inspect_compact(c: i64) {
5748    INSPECT_COMPACT.with(|x| x.set(c));
5749}
5750
5751/// Set the `util.inspect` `breakLength` for the next render.
5752pub fn set_inspect_break_length(n: usize) {
5753    INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5754}
5755
5756fn inspect_compact() -> i64 {
5757    INSPECT_COMPACT.with(|x| x.get())
5758}
5759
5760/// Set the `util.inspect` depth for the next render (restore to 2 after).
5761pub fn set_inspect_max_depth(d: i64) {
5762    INSPECT_MAX_DEPTH.with(|c| c.set(d));
5763}
5764/// Twice the configured depth, which is what the inspect walk compares its
5765/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5766/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5767/// overflowed and panicked the process — an abort no script could catch.
5768fn inspect_indent_limit() -> i64 {
5769    inspect_max_depth().saturating_mul(2)
5770}
5771
5772fn inspect_max_depth() -> i64 {
5773    INSPECT_MAX_DEPTH.with(|c| c.get())
5774}
5775
5776/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5777/// reinterpret as signed.
5778///
5779/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5780/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5781/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5782/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5783/// power-of-two modulus is exact for every finite double, so this is the whole
5784/// fix — and it is the form `Math.clz32` already used.
5785pub(crate) fn to_int32(f: f64) -> i32 {
5786    to_uint32(f) as i32
5787}
5788pub(crate) fn to_uint32(f: f64) -> u32 {
5789    if !f.is_finite() {
5790        return 0;
5791    }
5792    f.trunc().rem_euclid(4294967296.0) as u32
5793}
5794
5795/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5796fn str_to_number(s: &str) -> f64 {
5797    let t = crate::utf16::js_trim(s);
5798    if t.is_empty() {
5799        return 0.0;
5800    }
5801    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5802        return i64::from_str_radix(hex, 16)
5803            .map(|n| n as f64)
5804            .unwrap_or(f64::NAN);
5805    }
5806    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5807        return i64::from_str_radix(oct, 8)
5808            .map(|n| n as f64)
5809            .unwrap_or(f64::NAN);
5810    }
5811    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5812        return i64::from_str_radix(bin, 2)
5813            .map(|n| n as f64)
5814            .unwrap_or(f64::NAN);
5815    }
5816    match t {
5817        "Infinity" | "+Infinity" => f64::INFINITY,
5818        "-Infinity" => f64::NEG_INFINITY,
5819        _ => t.parse::<f64>().unwrap_or(f64::NAN),
5820    }
5821}
5822
5823/// `util.inspect` break length (the width past which entries wrap). Node's default.
5824fn break_length() -> usize {
5825    INSPECT_BREAK_LENGTH.with(|x| x.get())
5826}
5827/// Node's default `compact` setting (the `compact * 4` column cap term).
5828/// Node's DEFAULT `compact` setting, and the initial value of
5829/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5830/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5831/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5832const DEFAULT_COMPACT: i64 = 3;
5833/// Node's default `maxArrayLength` — the initial value of
5834/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5835/// overrides per call. Read it through `inspect_max_array_length()`, never
5836/// directly: as a bare constant the option had no effect and a 120-element array
5837/// was truncated at 100 even under `maxArrayLength: Infinity`.
5838pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5839
5840/// Whether `output` fits on a single line — a faithful port of Node's
5841/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5842/// length (braces + indentation + slack).
5843fn is_below_break_length(output: &[String], start: usize) -> bool {
5844    let limit = break_length();
5845    let mut total = output.len() + start;
5846    if total + output.len() > limit {
5847        return false;
5848    }
5849    for o in output {
5850        if o.contains('\n') {
5851            return false;
5852        }
5853        total += o.chars().count();
5854        if total > limit {
5855            return false;
5856        }
5857    }
5858    true
5859}
5860
5861/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5862/// already-formatted element strings into an aligned multi-column grid. Returns
5863/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5864/// ungrouped (so the caller falls back to single-line / one-per-line).
5865fn group_array_elements(
5866    host: &JsHost,
5867    output: &[String],
5868    values: &[Value],
5869    indentation_lvl: usize,
5870    has_tail: bool,
5871) -> (Vec<String>, bool) {
5872    let separator_space = 2usize; // ", " between entries
5873                                  // A `... N more items` tail is not an element: node drops it from the grid
5874                                  // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5875                                  // re-appends it as its own final line.
5876    let output_length = output.len() - usize::from(has_tail);
5877    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5878    let mut total_length = 0usize;
5879    let mut max_length = 0usize;
5880    for &len in &data_len[..output_length] {
5881        total_length += len + separator_space;
5882        if len > max_length {
5883            max_length = len;
5884        }
5885    }
5886    let actual_max = max_length + separator_space;
5887    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5888    if !(actual_max * 3 + indentation_lvl < break_length()
5889        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5890    {
5891        return (output.to_vec(), false);
5892    }
5893    let approx_char_heights = 2.5f64;
5894    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5895    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5896    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5897    let columns = [
5898        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5899            as i64,
5900        ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5901        inspect_compact().saturating_mul(4),
5902        15,
5903    ]
5904    .into_iter()
5905    .min()
5906    .unwrap();
5907    if columns <= 1 {
5908        return (output.to_vec(), false);
5909    }
5910    let columns = columns as usize;
5911    // The widest entry (plus separator) in each column.
5912    let mut max_line_length = vec![0usize; columns];
5913    for (i, slot) in max_line_length.iter_mut().enumerate() {
5914        let mut line_length = 0;
5915        let mut j = i;
5916        while j < output_length {
5917            if data_len[j] > line_length {
5918                line_length = data_len[j];
5919            }
5920            j += columns;
5921        }
5922        *slot = line_length + separator_space;
5923    }
5924    // Right-align (padStart) only when every element is a number/bigint.
5925    let pad_start = values.iter().all(|v| {
5926        matches!(v, Value::Int(_) | Value::Float(_))
5927            || matches!(host.get(v), Some(JsObj::BigInt(_)))
5928    });
5929    let mut tmp = Vec::new();
5930    let mut i = 0;
5931    while i < output_length {
5932        let max = (i + columns).min(output_length);
5933        let mut str_line = String::new();
5934        let mut j = i;
5935        while j < max.saturating_sub(1) {
5936            // `output[j]` has no colors here, so padding == max_line_length[col].
5937            let col = j - i;
5938            let cell = format!("{}, ", output[j]);
5939            let target = max_line_length[col];
5940            str_line.push_str(&pad_to(&cell, target, pad_start));
5941            j += 1;
5942        }
5943        // The last cell of the row: right-aligned entries pad without the ", ".
5944        if pad_start {
5945            let col = j - i;
5946            let target = max_line_length[col] - separator_space;
5947            str_line.push_str(&pad_to(&output[j], target, true));
5948        } else {
5949            str_line.push_str(&output[j]);
5950        }
5951        tmp.push(str_line);
5952        i += columns;
5953    }
5954    if has_tail {
5955        tmp.push(output[output_length].clone());
5956    }
5957    (tmp, true)
5958}
5959
5960/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5961/// (Padding is measured in chars; already ANSI-free here.)
5962fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5963    let len = s.chars().count();
5964    if len >= width {
5965        return s.to_string();
5966    }
5967    let fill = " ".repeat(width - len);
5968    if pad_start {
5969        format!("{fill}{s}")
5970    } else {
5971        format!("{s}{fill}")
5972    }
5973}
5974
5975/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5976/// `lib/internal/util/inspect.js`.
5977///
5978/// The quote character is chosen so the contents need as little escaping as
5979/// possible: single quotes normally, double quotes when the string contains a
5980/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5981/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5982/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5983/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5984fn quote_str(s: &str) -> String {
5985    let quote = if !s.contains('\'') {
5986        '\''
5987    } else if !s.contains('"') {
5988        '"'
5989    } else if !s.contains('`') && !s.contains("${") {
5990        '`'
5991    } else {
5992        '\''
5993    };
5994    let mut out = String::with_capacity(s.len() + 2);
5995    out.push(quote);
5996    for c in s.chars() {
5997        match c {
5998            _ if c == quote => {
5999                out.push('\\');
6000                out.push(c);
6001            }
6002            '\\' => out.push_str("\\\\"),
6003            '\u{8}' => out.push_str("\\b"),
6004            '\t' => out.push_str("\\t"),
6005            '\n' => out.push_str("\\n"),
6006            '\u{c}' => out.push_str("\\f"),
6007            '\r' => out.push_str("\\r"),
6008            '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
6009            _ => out.push(c),
6010        }
6011    }
6012    out.push(quote);
6013    out
6014}
6015
6016/// Render an object key: bare if it is a valid identifier, quoted otherwise.
6017fn fmt_key(k: &str) -> String {
6018    let ok = !k.is_empty()
6019        && k.chars()
6020            .next()
6021            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
6022            .unwrap_or(false)
6023        && k.chars()
6024            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
6025    if ok {
6026        k.to_string()
6027    } else {
6028        quote_str(k)
6029    }
6030}
6031
6032// ── iteration ────────────────────────────────────────────────────────────────
6033
6034impl JsHost {
6035    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
6036    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
6037    /// holds no host borrow across resumes.
6038    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
6039        match self.get(v) {
6040            Some(JsObj::Array(items)) => Ok(items.clone()),
6041            Some(JsObj::Str(s)) => {
6042                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
6043                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
6044            }
6045            // A live array iterator, read from where its cursor stands. The
6046            // backing slots only: `iter_all` steps one through `[[Get]]` instead.
6047            Some(JsObj::Iter {
6048                idx,
6049                array: Some((arr, kind)),
6050                ..
6051            }) => {
6052                let (idx, kind) = (*idx, *kind);
6053                let items = match self.get(arr) {
6054                    Some(JsObj::Array(items)) if idx < items.len() => items[idx..].to_vec(),
6055                    _ => Vec::new(),
6056                };
6057                Ok(items
6058                    .into_iter()
6059                    .enumerate()
6060                    .map(|(n, v)| {
6061                        let key = Value::Float((idx + n) as f64);
6062                        match kind {
6063                            ArrayIterKind::Keys => key,
6064                            ArrayIterKind::Values => v,
6065                            ArrayIterKind::Entries => self.new_array(vec![key, v]),
6066                        }
6067                    })
6068                    .collect())
6069            }
6070            Some(JsObj::Iter { items, idx, .. }) => Ok(items[*idx..].to_vec()),
6071            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
6072            Some(JsObj::Map { entries, .. }) => {
6073                // Map iterates as `[key, value]` pairs.
6074                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
6075                Ok(pairs
6076                    .into_iter()
6077                    .map(|(k, v)| self.new_array(vec![k, v]))
6078                    .collect())
6079            }
6080            // A `Buffer` iterates over its BYTES and a typed array over its
6081            // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
6082            // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
6083            // "object is not iterable", which is the same invariant holding at
6084            // one of its two sites.
6085            Some(JsObj::Object(props))
6086                if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
6087            {
6088                // Iterating a view over a DETACHED buffer throws, naming the
6089                // `values` iterator — reading its elements answers zero length,
6090                // but spreading it is a method call and does not.
6091                if crate::stdlib::typedarray::view_detached_h(self, v) {
6092                    return Err(crate::stdlib::typedarray::detached_error(
6093                        "%TypedArray%.prototype",
6094                        "values",
6095                        false,
6096                    ));
6097                }
6098                Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
6099            }
6100            // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
6101            // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
6102            // produced `number is not iterable`, which no engine emits.
6103            _ => {
6104                let shown = self.inspect(v);
6105                Err(type_error(&format!("{shown} is not iterable")))
6106            }
6107        }
6108    }
6109
6110    /// Enumerable string keys of an object/array (for `for-in`). Internal
6111    /// symbol-keyed props (`@@…`) are not enumerable.
6112    /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
6113    /// not already seen, walking the whole prototype chain. Class methods and the
6114    /// builtin prototypes are non-enumerable, so in practice this only surfaces
6115    /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
6116    /// is exactly the constructor-function idiom older packages are written in.
6117    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
6118        let mut keys = self.own_enum_key_names(v);
6119        let mut cur = self.proto_of(v);
6120        let mut hops = 0;
6121        while let Some(p) = cur {
6122            // A cyclic or pathologically deep chain must not hang the loop.
6123            hops += 1;
6124            if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
6125                break;
6126            }
6127            for k in self.own_enum_key_names(&p) {
6128                if !keys.contains(&k) {
6129                    keys.push(k);
6130                }
6131            }
6132            cur = self.proto_of(&p);
6133        }
6134        keys.into_iter().map(|k| self.new_str(k)).collect()
6135    }
6136
6137    /// The own *enumerable* string keys of `v`, in property order — the single
6138    /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6139    /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6140    /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6141    /// `prop_attrs` are excluded.
6142    pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6143        self.own_key_names(v, true)
6144    }
6145
6146    /// Own string keys of `v` in insertion order. `enum_only` drops the
6147    /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6148    /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6149    pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6150        let mut keys = self.own_enum_data_keys(v, enum_only);
6151        // A global a SCRIPT created (`x = 1` with no declaration) is an own
6152        // ENUMERABLE property of the global object, but lives in the globals map
6153        // rather than in its property map — so no listing saw it, while
6154        // `globalThis.x` read it back and its descriptor called it enumerable.
6155        if self.is_global_object(v) {
6156            for k in self.globals.keys() {
6157                if !keys.contains(k) {
6158                    keys.push(k.clone());
6159                }
6160            }
6161        }
6162        // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6163        // `RegExpObj` struct, not a property map — so nothing above can list it.
6164        // Non-enumerable, so only `getOwnPropertyNames` sees it.
6165        if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6166            keys.push("lastIndex".to_string());
6167        }
6168        // An accessor defined before its object had any ordering marker (a class
6169        // prototype accessor, say) still has to appear.
6170        for k in self.own_accessor_keys(v) {
6171            if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6172                keys.push(k);
6173            }
6174        }
6175        keys
6176    }
6177
6178    /// The keys that own a slot in the object's property map, in insertion
6179    /// order, resolving accessor ordering markers back to their real key.
6180    /// Every global a SCRIPT created, in creation order — the own enumerable
6181    /// keys of the global object that live in the globals map rather than in
6182    /// its property map. `x = 1` with no declaration makes one, and
6183    /// `Object.keys(globalThis)` reports it in node.
6184    pub fn script_global_names(&self) -> Vec<String> {
6185        self.globals.keys().cloned().collect()
6186    }
6187    /// Drop a global a script created. Reports whether it was there.
6188    pub fn remove_global(&mut self, name: &str) -> bool {
6189        self.globals.shift_remove(name).is_some()
6190    }
6191    fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6192        match self.get(v) {
6193            // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6194            // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6195            // the `length`/`byteLength` view metadata, which V8 keeps on the
6196            // prototype chain or as non-enumerable own slots.
6197            // A `Buffer` and every other typed array are index-keyed exotics:
6198            // their own enumerable keys are `"0".."len-1"` (the elements live in
6199            // a hidden slot), never the `length`/`byteLength` view metadata,
6200            // which V8 keeps on the prototype chain or as non-enumerable own
6201            // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6202            // and `JSON.stringify(u8)` was `{}` where node gives
6203            // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6204            // the two views of the same question disagreed.
6205            Some(JsObj::Object(props))
6206                if matches!(
6207                    props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6208                    Some("Buffer") | Some("TypedArray")
6209                ) =>
6210            {
6211                // A view over a DETACHED buffer has no index properties at all:
6212                // its own `length` still holds the old count, so reading that
6213                // back left `Object.keys` listing eight names over no bytes.
6214                if crate::stdlib::typedarray::view_detached_h(self, v) {
6215                    return Vec::new();
6216                }
6217                // A Buffer counts its byte store; every other view reports the
6218                // element count of its window onto the ArrayBuffer.
6219                let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6220                    Some(JsObj::Array(items)) => items.len(),
6221                    _ => props
6222                        .get("length")
6223                        .map(|l| self.to_number(l))
6224                        .unwrap_or(0.0) as usize,
6225                };
6226                (0..n).map(|i| i.to_string()).collect()
6227            }
6228            Some(JsObj::Object(props)) => props
6229                .keys()
6230                .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6231                    Some(real) => Some(real.to_string()),
6232                    None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6233                    None => None,
6234                })
6235                .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6236                .collect(),
6237            // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6238            // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6239            // Without this arm every whole-object view of a string primitive was
6240            // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6241            // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6242            // `'ab'.length` answered normally, so the two views disagreed. The
6243            // spread form `{...'ab'}` went through a different path and was
6244            // already right, which is what made the gap easy to miss.
6245            Some(JsObj::Str(s)) => {
6246                let mut keys: Vec<String> =
6247                    (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6248                if !enum_only {
6249                    keys.push("length".into());
6250                }
6251                keys
6252            }
6253            // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6254            // ascending, then the exotic non-enumerable `length`, then the
6255            // ordinary string keys in insertion order. Those ordinary keys have
6256            // no property map to live in — a `str.match()` result's
6257            // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6258            // in the fn-prop side table — so they are read back from there.
6259            Some(JsObj::Array(items)) => {
6260                // An ELIDED element is not an own property at all, so it
6261                // contributes no key — the difference behind
6262                // `Object.keys([1,,3])` being `['0','2']`.
6263                let mut keys: Vec<String> = (0..items.len())
6264                    .filter(|i| !self.is_hole(v, *i))
6265                    .map(|i| i.to_string())
6266                    .collect();
6267                if !enum_only {
6268                    keys.push("length".into());
6269                }
6270                keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6271                    !k.starts_with("@@")
6272                        && !k.starts_with('#')
6273                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6274                }));
6275                keys
6276            }
6277            // A function/class keeps every own property in the side table. Its
6278            // exotic `name`/`length`/`prototype` and its class methods are all
6279            // non-enumerable, so under `enum_only` what is left is exactly what
6280            // a script assigned; `getOwnPropertyNames` reports the exotics too,
6281            // in V8's order (`length`, `name`, `prototype`, then the rest).
6282            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6283                let mut keys: Vec<String> = Vec::new();
6284                if !enum_only {
6285                    keys.push("length".into());
6286                    keys.push("name".into());
6287                    if self.owns_prototype(v) {
6288                        keys.push("prototype".into());
6289                    }
6290                }
6291                let rest: Vec<String> = self
6292                    .fn_prop_keys(v)
6293                    .into_iter()
6294                    // An accessor's ordering marker resolves back to its real
6295                    // key, so a static getter enumerates where it was declared.
6296                    .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6297                        Some(real) => Some(real.to_string()),
6298                        None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6299                        None => None,
6300                    })
6301                    .filter(|k| {
6302                        !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6303                    })
6304                    .collect();
6305                keys.extend(rest);
6306                keys
6307            }
6308            // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6309            // members node-js implements, so a package that copies a namespace
6310            // key-by-key gets the working set instead of an empty object.
6311            Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6312            // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6313            // slots, so what a script assigned lives in the side table — and is
6314            // just as much an own property as an object's.
6315            Some(_) => self
6316                .fn_prop_keys(v)
6317                .into_iter()
6318                .filter(|k| {
6319                    !k.starts_with("@@")
6320                        && !k.starts_with('#')
6321                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6322                })
6323                .collect(),
6324            _ => Vec::new(),
6325        }
6326    }
6327
6328    /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6329    /// through the byte store; everything else reads the property map. Own
6330    /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6331    /// their getters, which cannot happen under the host borrow.
6332    pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6333        self.own_enum_key_names(v)
6334            .into_iter()
6335            .map(|k| {
6336                let val = match self.get(v) {
6337                    // A Buffer's index keys read out of the hidden `@@bytes`
6338                    // array; resolve inline rather than through
6339                    // `buffer::byte_get`, which would re-borrow the host.
6340                    Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6341                        // A Buffer's elements live in `@@bytes` and every
6342                        // other typed array's in `@@elems`; both are index
6343                        // keys with no entry in the property map.
6344                        match k.parse::<usize>() {
6345                            Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6346                                .get(i)
6347                                .cloned()
6348                                .unwrap_or(Value::Undef),
6349                            _ => Value::Undef,
6350                        }
6351                    }),
6352                    // A Map/Set/Promise/RegExp/generator keeps every own
6353                    // property in the side table.
6354                    Some(
6355                        JsObj::Map { .. }
6356                        | JsObj::Set { .. }
6357                        | JsObj::Promise { .. }
6358                        | JsObj::RegExp(_)
6359                        | JsObj::Generator { .. }
6360                        | JsObj::Symbol { .. }
6361                        | JsObj::BigInt(_)
6362                        | JsObj::Iter { .. },
6363                    ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6364                    // An index reads the element; any other own key (`foo`,
6365                    // a match result's `index`) lives in the side table.
6366                    Some(JsObj::Array(items)) => k
6367                        .parse::<usize>()
6368                        .ok()
6369                        .and_then(|i| items.get(i).cloned())
6370                        .or_else(|| self.fn_prop(v, &k))
6371                        .unwrap_or(Value::Undef),
6372                    Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6373                        self.fn_prop(v, &k).unwrap_or(Value::Undef)
6374                    }
6375                    _ => Value::Undef,
6376                };
6377                (k, val)
6378            })
6379            .collect()
6380    }
6381}
6382
6383/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6384/// accessor's getter invoked — the observable shape `Object.values`,
6385/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6386/// outside a `with_host` borrow because a getter re-enters the host.
6387pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6388    // A Proxy has no property map at all: its own enumerable entries come from
6389    // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6390    // throws surfaces as an empty result here because this signature is
6391    // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6392    // and friends) go through `builtins::object_keys`, which does.
6393    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6394        return crate::proxy::own_enum_entries(v);
6395    }
6396    // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6397    // all: its members are resolved on demand by `namespace_property`, which
6398    // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6399    // Without this, spread and `Object.assign` copied the namespace's KEYS with
6400    // `undefined` for every value — measured against node v26.7.0,
6401    // `{...require('path')}.join` was `undefined` here and a function there,
6402    // while `Object.entries(require('path'))` (which resolves through
6403    // `builtins`, not through this borrow) was already correct. Two enumeration
6404    // paths, one of them silently value-less.
6405    if let Some(ns) = with_host(|h| match h.get(v) {
6406        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6407        _ => None,
6408    }) {
6409        return Ok(with_host(|h| h.own_enum_key_names(v))
6410            .into_iter()
6411            .map(|k| {
6412                let val = crate::builtins::namespace_property(&ns, &k);
6413                (k, val)
6414            })
6415            .collect());
6416    }
6417    // A string primitive's own entries are its code units. `own_enum_entries`
6418    // cannot build them: allocating the one-character string for each index
6419    // needs `&mut` host access, and it runs under a shared borrow.
6420    if let Some(sv) = with_host(|h| match h.get(v) {
6421        Some(JsObj::Str(s)) => Some(s.clone()),
6422        _ => None,
6423    }) {
6424        let units = crate::utf16::Units::of(&sv);
6425        return Ok(with_host(|h| {
6426            (0..units.len())
6427                .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6428                .collect()
6429        }));
6430    }
6431    let accessor_keys: Vec<String> = with_host(|h| {
6432        h.own_accessor_keys(v)
6433            .into_iter()
6434            .filter(|k| h.prop_attrs(v, k).enumerable)
6435            .collect()
6436    });
6437    let entries = with_host(|h| h.own_enum_entries(v));
6438    // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6439    // object spread and `JSON.stringify` all read through here, and every one
6440    // of them swallowed the exception and reported the property as absent (or
6441    // as `null`) instead.
6442    let mut out = Vec::with_capacity(entries.len());
6443    for (k, val) in entries {
6444        if accessor_keys.contains(&k) {
6445            out.push((k.clone(), get_prop_chain(v, &k)?));
6446        } else {
6447            out.push((k, val));
6448        }
6449    }
6450    Ok(out)
6451}
6452
6453// ── function invocation ──────────────────────────────────────────────────────
6454
6455/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6456/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6457/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6458/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6459/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6460/// Float→i64/f64 per the export signature).
6461fn marshal_ffi_arg(v: &Value) -> Value {
6462    match v {
6463        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6464            Some(s) => Value::str(s),
6465            None => v.clone(),
6466        },
6467        _ => v.clone(),
6468    }
6469}
6470
6471/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6472pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6473    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6474    // line)`; compile + register the block's exported functions, returning JS
6475    // `undefined` (`Value::Undef`).
6476    if name == "__rust_compile" {
6477        let b64 = args
6478            .first()
6479            .map(|v| with_host(|h| h.str_of(v)))
6480            .unwrap_or_default();
6481        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6482    }
6483    if let Some(v) = with_host(|h| h.read_name(name)) {
6484        return invoke(&v, args, None);
6485    }
6486    // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6487    // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6488    // and `[eval][0](src)` all reach the same function value but are INDIRECT
6489    // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6490    // This is the one place the two forms are distinguishable without a compiler
6491    // change: `call_named` is reached only from `ops::CALL`, which the compiler
6492    // emits exclusively for a bare-identifier callee, while every value-call form
6493    // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6494    // has already established that `eval` is not shadowed by a user binding.
6495    if name == "eval" {
6496        return crate::builtins::eval_source(args.first(), true);
6497    }
6498    if crate::builtins::is_known_builtin(name) {
6499        return crate::builtins::call_builtin_function(name, args);
6500    }
6501    // A `rust { ... }` block's exported functions are callable by bareword.
6502    // Reached only after user names/globals and builtins all miss, so JS code
6503    // always wins; the registry membership check keeps this off the hot path.
6504    if fusevm::ffi::is_registered(name) {
6505        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6506        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6507            return r;
6508        }
6509    }
6510    Err(ref_error(name))
6511}
6512
6513thread_local! {
6514    /// The constructor a builtin STATIC is currently being invoked on.
6515    ///
6516    /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6517    /// builtin, which is reached by NAME and so cannot see `A`. The species
6518    /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6519    /// their result with `this`, so on a subclass they must construct through
6520    /// it. A stack, since one static can call another.
6521    static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6522        const { std::cell::RefCell::new(Vec::new()) };
6523}
6524
6525/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6526pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6527    STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6528    let out = f();
6529    STATIC_THIS.with(|s| {
6530        s.borrow_mut().pop();
6531    });
6532    out
6533}
6534
6535/// The constructor the running builtin static was called on, if it was reached
6536/// through a subclass rather than directly.
6537pub fn current_static_this() -> Option<Value> {
6538    STATIC_THIS.with(|s| s.borrow().last().cloned())
6539}
6540
6541/// `recv.name(args)`.
6542pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6543    // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6544    // the failure is the property read, not the call: node reports
6545    // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6546    // whole method dispatch against the nullish receiver, found nothing, and
6547    // reported `undefined.foo is not a function` — the wrong error class of
6548    // message for the single most common runtime fault in JS, and one that
6549    // points at the callee instead of at the base that was nullish.
6550    if with_host(|h| h.is_nullish(recv)) {
6551        return Err(type_error(&format!(
6552            "Cannot read properties of {} (reading '{name}')",
6553            with_host(|h| h.str_of(recv))
6554        )));
6555    }
6556    // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6557    // comes first: an unbranded receiver throws here rather than reporting the
6558    // method missing. Only a `#`-prefixed name pays the extra probe.
6559    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6560        return Err(crate::builtins::private_brand_message(name, false));
6561    }
6562    // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6563    // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6564    // below all read a property map a proxy does not have.
6565    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6566        let f = crate::builtins::get_property(recv, name)?;
6567        if !with_host(|h| is_callable(h, &f)) {
6568            return Err(type_error(&format!("{name} is not a function")));
6569        }
6570        // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6571        // `Object.prototype` methods are generic over `this`. node-js models each
6572        // as a thunk BOUND to the object it was read off — through a proxy, that
6573        // is the target — so invoking the thunk answers for the target and skips
6574        // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6575        // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6576        // those against the PROXY, which is the `this` the real method receives.
6577        //
6578        // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6579        // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6580        // an array stringifies `1,2` through `Array.prototype.toString`, not
6581        // `[object Object]`), which the bound thunk already gets right.
6582        if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6583            if with_host(|h| is_callable(h, recv)) {
6584                if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6585                    return Ok(r);
6586                }
6587            }
6588            if matches!(
6589                name,
6590                "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6591            ) {
6592                return crate::builtins::object_builtin_method(recv, name, args);
6593            }
6594            // The three above resolve by the TARGET's kind, and the thunk is
6595            // already bound to the target — so it must be invoked WITHOUT a
6596            // receiver override. Passing the proxy as `this` made the
6597            // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6598            // call straight back into this branch, so `String(new Proxy({}, {}))`
6599            // recursed until the stack overflowed and the process aborted.
6600            if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6601                return invoke(&f, args, None);
6602            }
6603        }
6604        return invoke(&f, args, Some(recv.clone()));
6605    }
6606    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6607    // name.
6608    if let Some(ns) = with_host(|h| match h.get(recv) {
6609        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6610        _ => None,
6611    }) {
6612        let qualified = format!("{ns}.{name}");
6613        if crate::builtins::is_known_builtin(&qualified) {
6614            return crate::builtins::call_builtin_function(&qualified, args);
6615        }
6616    }
6617    // Object / instance: an accessor getter that yields a function, an own or
6618    // inherited method (class methods live on the prototype chain), then an
6619    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6620    // directly — NOT get_property — so the Object.prototype-builtin fallback
6621    // never routes back through a BoundMethod and recurses.
6622    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6623        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6624        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6625        // A user-added or reparented-prototype method takes precedence over the
6626        // native dispatcher — matching JS resolution order (own → prototype
6627        // chain). This is what lets Express work: it does
6628        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6629        // where `send` is a plain function on the reparented prototype. Native
6630        // instance methods (`res.end`/`write`/…) are NOT stored as plain
6631        // function properties, so `lookup_chain` misses them and we fall through
6632        // to `instance_call` for the real native behavior.
6633        if let Some(tag) = crate::stdlib::native_tag(recv) {
6634            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6635                if with_host(|h| is_callable(h, &f)) {
6636                    return invoke(&f, args, Some(recv.clone()));
6637                }
6638            }
6639            // `Object.prototype` methods reach a native instance too — a Buffer
6640            // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6641            // chain, and the native dispatcher has no entry for them.
6642            if crate::builtins::is_object_builtin_method(name)
6643                && !crate::stdlib::instance_has_method(&tag, name)
6644            {
6645                return crate::builtins::object_builtin_method(recv, name, args);
6646            }
6647            return crate::stdlib::instance_call(&tag, recv, name, args);
6648        }
6649        // A primitive wrapper forwards to the primitive's method table, the
6650        // same way a native instance forwards to its tag's. A user method on
6651        // the wrapper or anywhere on its chain still wins first.
6652        if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6653            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6654                if with_host(|h| is_callable(h, &f)) {
6655                    return invoke(&f, args, Some(recv.clone()));
6656                }
6657            }
6658            // The reflective `Object.prototype` methods answer for the WRAPPER
6659            // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6660            // properties, not about the string.
6661            if crate::builtins::is_object_builtin_method(name) {
6662                return crate::builtins::object_builtin_method(recv, name, args);
6663            }
6664            return call_method(&prim, name, args);
6665        }
6666        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6667            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6668            if with_host(|h| is_callable(h, &f)) {
6669                return invoke(&f, args, Some(recv.clone()));
6670            }
6671        }
6672        // A Proxy in the prototype chain serves the method through its `get`
6673        // trap. `lookup_chain` below reads property maps, which a proxy has none
6674        // of, so without this `child.m()` on `Object.create(proxy)` reported
6675        // "m is not a function" even though `child.m` already read correctly.
6676        if crate::builtins::proxy_proto_link(recv, name).is_some() {
6677            let f = crate::builtins::get_property(recv, name)?;
6678            if !with_host(|h| is_callable(h, &f)) {
6679                return Err(type_error(&format!("{name} is not a function")));
6680            }
6681            return invoke(&f, args, Some(recv.clone()));
6682        }
6683        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6684            if with_host(|h| is_callable(h, &f)) {
6685                return invoke(&f, args, Some(recv.clone()));
6686            }
6687            return Err(type_error(&format!("{name} is not a function")));
6688        }
6689        // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6690        // it: a plain object is not LINKED to the intrinsic prototype object,
6691        // its `Object.prototype` members are synthesized instead. So
6692        // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6693        // while `({}).tap` already read back as `f`.
6694        if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6695            if with_host(|h| is_callable(h, &f)) {
6696                return invoke(&f, args, Some(recv.clone()));
6697            }
6698        }
6699        // A method from an intrinsic prototype this object's CHAIN passes
6700        // through — `Object.create(Array.prototype).push(1)`. The read already
6701        // resolves it through the same owner oracle; dispatch reported "is not
6702        // a function", the read and the call disagreeing once more.
6703        if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6704            if owner != "Object" {
6705                return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6706            }
6707        }
6708        if crate::builtins::is_object_builtin_method(name) {
6709            return crate::builtins::object_builtin_method(recv, name, args);
6710        }
6711        if name == "constructor" {
6712            if let Some(r) = call_default_ctor(recv, &args) {
6713                return r;
6714            }
6715        }
6716        return Err(type_error(&format!("{name} is not a function")));
6717    }
6718    // Function value methods: call / apply / bind, then any static method stored
6719    // on the function object.
6720    if matches!(
6721        with_host(|h| h.kind_of(recv)),
6722        Some(ObjKind::Func)
6723            | Some(ObjKind::Class)
6724            | Some(ObjKind::BoundFunc)
6725            | Some(ObjKind::BoundMethod)
6726            | Some(ObjKind::Builtin)
6727    ) {
6728        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6729            return Ok(r);
6730        }
6731        // A static method (own or inherited): `this` is the constructor (`recv`).
6732        let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6733            with_host(|h| h.class_static(recv, name))
6734        } else {
6735            with_host(|h| h.fn_prop(recv, name))
6736        };
6737        if let Some(f) = stat {
6738            if with_host(|h| is_callable(h, &f)) {
6739                return invoke(&f, args, Some(recv.clone()));
6740            }
6741        }
6742        // `class_static` only walks user-class `extends` links, so a chain that
6743        // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6744        // could not reach that builtin's statics: `D.from([1,2])` threw
6745        // "from is not a function" even though `typeof D.from` said `function`.
6746        // Re-dispatch the call against that ancestor, which is what reaches a
6747        // builtin namespace's methods.
6748        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6749            if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6750                if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6751                    // The subclass is recorded so a species-aware static
6752                    // (`Array.from`, `Promise.resolve`, …) builds its result
6753                    // through it rather than through the builtin.
6754                    return with_static_this(recv, || call_method(&anc, name, args));
6755                }
6756            }
6757        }
6758        // A method inherited via the function's [[Prototype]] chain (set with
6759        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6760        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6761        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6762            if with_host(|h| is_callable(h, &f)) {
6763                return invoke(&f, args, Some(recv.clone()));
6764            }
6765        }
6766        // An `Object.prototype` method invoked with a builtin namespace/prototype
6767        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6768        // ownership probe) — dispatch it against the builtin receiver.
6769        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6770            && crate::builtins::is_object_builtin_method(name)
6771        {
6772            return crate::builtins::object_builtin_method(recv, name, args);
6773        }
6774    }
6775    if name == "constructor" {
6776        if let Some(r) = call_default_ctor(recv, &args) {
6777            return r;
6778        }
6779    }
6780    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6781    crate::builtins::call_type_method(recv, name, args)
6782}
6783
6784/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6785/// owns a `constructor` slot.
6786///
6787/// Reading the property already resolves a builtin instance's native constructor
6788/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6789/// consulted the prototype chain, so the two disagreed:
6790/// `(function(){}).constructor === Function` read `true` while
6791/// `(function(){}).constructor('return 9')` threw
6792/// `TypeError: constructor is not a function`. That call form is exactly how
6793/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6794/// constructor. Resolved here through the same one definition the read uses, so
6795/// the two can no longer drift apart. `None` means "not resolvable/callable",
6796/// leaving the caller's original error in place.
6797fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6798    let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6799    with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6800}
6801
6802/// Call any callable value.
6803pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6804    // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6805    // Probed by kind first so the ordinary call path never clones its arguments.
6806    if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6807        return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6808    }
6809    let obj = with_host(|h| h.get(callable).cloned());
6810    match obj {
6811        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6812        // against the invoke-time `this` (supplied by `.call`/`.apply`).
6813        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6814            let recv = this.unwrap_or(Value::Undef);
6815            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6816        }
6817        // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6818        // form a library uses to read a slot from an arbitrary receiver
6819        // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6820        // .call(m)`). It brand-checks `this` and reads, or throws naming
6821        // itself.
6822        // The setter half of the `arguments`/`caller` poison pill — the only
6823        // intrinsic accessor here that has one, and it throws like its getter.
6824        Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6825            let _ = &name;
6826            let recv = this.unwrap_or(Value::Undef);
6827            // The setter half accepts silently for the same receivers the
6828            // getter answers for, and throws for the rest.
6829            if with_host(|h| h.fn_is_sloppy(&recv)) {
6830                Ok(Value::Undef)
6831            } else {
6832                Err(type_error(crate::builtins::POISON_PILL))
6833            }
6834        }
6835        Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6836            let recv = this.unwrap_or(Value::Undef);
6837            let rest = &name["@protoget:".len()..];
6838            let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6839            crate::builtins::proto_getter_call(ctor, key, &recv)
6840        }
6841        // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6842        // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6843        // this:
6844        //
6845        //     function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6846        //     InternalDecoder.prototype = StringDecoder.prototype;
6847        //
6848        // A native constructor builds a fresh tagged object, so initializing the
6849        // SUPPLIED object means building one and moving its slots across.
6850        //
6851        // The guard is deliberately narrow: `obj` must already inherit from THIS
6852        // constructor's prototype, i.e. the subclass really did adopt it. Without
6853        // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6854        // return a string / a buffer — would start mutating `x` instead.
6855        Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6856            let target = this.expect("guard checked");
6857            let built = crate::stdlib::construct(name, &args)
6858                .expect("guard checked a native constructor")?;
6859            adopt_native_slots(&target, &built);
6860            Ok(Value::Undef)
6861        }
6862        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6863        Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6864        // A method read off an object is modelled as a thunk BOUND to it, but an
6865        // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6866        // rebinds `this`, and every `Array.prototype` method is generic over it, so
6867        // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6868        // override made that read back as the empty array the thunk was read off.
6869        // A nullish override is ignored: it carries no receiver to dispatch on.
6870        Some(JsObj::BoundMethod { recv, name }) => {
6871            let target = match &this {
6872                Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6873                _ => &recv,
6874            };
6875            // A thunk read off an ARRAY carries an `Array.prototype` method, and
6876            // those are generic over `this` — route the rebound call through
6877            // `proto_method` so an array-LIKE receiver takes the generic path
6878            // instead of being told the method does not exist.
6879            if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6880                return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6881            }
6882            call_method(target, &name, args)
6883        }
6884        Some(JsObj::BoundFunc {
6885            target,
6886            this: bthis,
6887            args: pre,
6888        }) => {
6889            let mut all = pre;
6890            all.extend(args);
6891            invoke(&target, all, Some(bthis))
6892        }
6893        Some(JsObj::Class(c)) => Err(type_error(&format!(
6894            "Class constructor {} cannot be invoked without 'new'",
6895            c.name
6896        ))),
6897        _ => Err(type_error(&format!(
6898            "{} is not a function",
6899            with_host(|h| h.str_of(callable))
6900        ))),
6901    }
6902}
6903
6904/// Whether calling the native constructor `name` with `this` is the ES5
6905/// constructor-stealing pattern rather than an ordinary call.
6906///
6907/// True only when `name` really is a native stdlib constructor AND `this` is a
6908/// plain object that already inherits from that constructor's prototype — the
6909/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6910/// object that merely happens to be passed as `this` does not qualify, so
6911/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6912fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6913    let Some(target) = this else { return false };
6914    if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6915        return false;
6916    }
6917    // Already initialized (e.g. a re-entrant call) — nothing to steal.
6918    if crate::stdlib::native_tag(target).is_some() {
6919        return false;
6920    }
6921    let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6922        return false;
6923    };
6924    let mut cur = with_host(|h| h.proto_of(target));
6925    while let Some(p) = cur {
6926        if p == proto {
6927            return true;
6928        }
6929        cur = with_host(|h| h.proto_of(&p));
6930    }
6931    false
6932}
6933
6934/// Move a freshly-constructed native instance's state onto `target`, so an
6935/// object built by a subclass constructor becomes a working instance of the
6936/// native class. Copies every own key the native constructor set — the hidden
6937/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6938/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6939fn adopt_native_slots(target: &Value, built: &Value) {
6940    let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6941        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6942        _ => Vec::new(),
6943    });
6944    with_host(|h| {
6945        if let Some(JsObj::Object(p)) = h.get_mut(target) {
6946            for (k, v) in slots {
6947                p.insert(k, v);
6948            }
6949        }
6950    });
6951}
6952
6953/// Execute a user function/closure body on a fresh frame.
6954pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6955    run_user_func_of(fv, args, this, None)
6956}
6957
6958/// [`run_user_func`] with the function VALUE the call came through, which the
6959/// `arguments` object needs for its `callee`.
6960pub fn run_user_func_of(
6961    fv: &FuncVal,
6962    args: Vec<Value>,
6963    this: Option<Value>,
6964    callee: Option<Value>,
6965) -> Result<Value, String> {
6966    run_user_func_full(fv, args, this, None, callee)
6967}
6968
6969/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6970pub fn run_user_func_nt(
6971    fv: &FuncVal,
6972    args: Vec<Value>,
6973    this: Option<Value>,
6974    new_target: Option<Value>,
6975) -> Result<Value, String> {
6976    run_user_func_full(fv, args, this, new_target, None)
6977}
6978
6979fn run_user_func_full(
6980    fv: &FuncVal,
6981    args: Vec<Value>,
6982    this: Option<Value>,
6983    new_target: Option<Value>,
6984    callee: Option<Value>,
6985) -> Result<Value, String> {
6986    // Consumed first, before anything here can start another call.
6987    let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6988    // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6989    // the entire compiled body, `sub_chunks` and all — on every single call.
6990    // The chunk is now reached once per pooled VM, in the two arms below.
6991    let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6992        let d = &h.funcs[fv.def_id];
6993        (
6994            d.params.clone(),
6995            d.is_generator,
6996            d.is_async,
6997            d.is_arrow,
6998            d.name.clone(),
6999        )
7000    });
7001    let env = new_env(fv.env.clone());
7002    // Bind the simple/rest arg slots; destructuring + defaults run in the body
7003    // prologue (compiled ahead of the user statements).
7004    let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
7005    bind_params(
7006        &env,
7007        &params,
7008        args,
7009        is_arrow_def,
7010        callee.as_ref(),
7011        fn_is_sloppy && !is_arrow_def,
7012    );
7013    // Arrow functions capture `this` lexically; regular functions receive it.
7014    let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
7015    // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
7016    // becomes the global object. Only a strict function keeps `undefined`, and
7017    // an arrow has no `this` of its own to substitute. Leaving it undefined
7018    // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
7019    // saw `undefined` where node sees `globalThis`.
7020    let sloppy_this = !fv.is_arrow
7021        && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
7022        && match &this_val {
7023            None => true,
7024            Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
7025        };
7026    if sloppy_this {
7027        this_val = Some(with_host(|h| h.global_object()));
7028    } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
7029        // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
7030        // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
7031        // wrapper rather than the number. Only strict mode passes it through.
7032        if let Some(t) = this_val.clone() {
7033            let boxed = crate::builtins::to_object(&t);
7034            this_val = Some(boxed);
7035        }
7036    }
7037    // A generator function does not run its body on call — it returns a suspended
7038    // generator over the already-bound frame.
7039    if is_generator {
7040        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
7041        let gen = make_generator(
7042            chunk,
7043            env,
7044            this_val,
7045            fv.home_class.clone(),
7046            fv.home_static,
7047            fv.home_object.clone(),
7048            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
7049        );
7050        if is_async {
7051            if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
7052                with_host(|h| h.generators[id as usize].async_gen = true);
7053            }
7054        }
7055        return Ok(gen);
7056    }
7057    // An async function runs on a coroutine and returns a Promise: it executes
7058    // synchronously up to the first `await`, then continues via microtasks.
7059    if is_async {
7060        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
7061        let gen = make_generator(
7062            chunk,
7063            env,
7064            this_val,
7065            fv.home_class.clone(),
7066            fv.home_static,
7067            fv.home_object.clone(),
7068            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
7069        );
7070        return Ok(run_async(gen));
7071    }
7072    let home = fv
7073        .home_class
7074        .as_ref()
7075        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7076    // Resolved BEFORE the borrow below: reading the function table re-enters
7077    // the host, and doing it inside the frame-push closure double-borrows.
7078    let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
7079    with_host(|h| {
7080        h.frames.push(Frame {
7081            base_env: env.clone(),
7082            env,
7083            this_obj: this_val,
7084            new_target,
7085            home_class: home,
7086            home_static: fv.home_static,
7087            home_object: fv.home_object.clone(),
7088            strict: fn_strict,
7089            line: 0,
7090            owner: Some(def_name),
7091            is_module: false,
7092            this_state: if derived_ctor {
7093                ThisState::Pending
7094            } else {
7095                ThisState::Plain
7096            },
7097        })
7098    });
7099    let r = run_chunk_keyed(func_key(fv.def_id), || {
7100        with_host(|h| h.funcs[fv.def_id].chunk.clone())
7101    });
7102    let (sig, this_state) = with_host(|h| {
7103        let frame = h.frames.pop();
7104        (h.signal.take(), frame.map(|f| f.this_state))
7105    });
7106    let ret = match r {
7107        Err(e) => return Err(e),
7108        Ok(_) => match sig {
7109            Some(Signal::Return(v)) => v,
7110            _ => Value::Undef,
7111        },
7112    };
7113    // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
7114    // return wins; any other non-undefined return is a TypeError; and falling
7115    // off the end (or `return;`) needs `this` to have been bound by `super()`.
7116    if derived_ctor && !returns_object(&ret) {
7117        if !matches!(ret, Value::Undef) {
7118            return Err(type_error(
7119                "Derived constructors may only return object or undefined",
7120            ));
7121        }
7122        if this_state == Some(ThisState::Pending) {
7123            return Err(this_before_super_error());
7124        }
7125    }
7126    Ok(ret)
7127}
7128
7129/// Bind positional args into a fresh call environment. The compiler emits the
7130/// param names in `def.params`; a `...rest` slot collects the tail as an array.
7131fn bind_params(
7132    env: &Env,
7133    params: &[ParamSlot],
7134    args: Vec<Value>,
7135    is_arrow: bool,
7136    callee: Option<&Value>,
7137    sloppy: bool,
7138) {
7139    let mut vars = VarMap::default();
7140    let mut i = 0;
7141    for slot in params {
7142        if slot.rest {
7143            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7144            let arr = with_host(|h| h.new_array(rest));
7145            vars.insert(slot.name.clone(), arr);
7146        } else {
7147            let v = args.get(i).cloned().unwrap_or(Value::Undef);
7148            vars.insert(slot.name.clone(), v);
7149            i += 1;
7150        }
7151    }
7152    // `arguments` array (simple approximation — see BUGS.md: it is a real
7153    // Array, not an Arguments exotic). An ARROW function never gets one:
7154    // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7155    // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7156    // enclosing function's. Binding an empty one here made
7157    // `function f(){ const g = () => [...arguments]; }` see zero args.
7158    if !is_arrow {
7159        let args_arr = with_host(|h| {
7160            let a = h.new_array(args);
7161            // Marked so it can be told apart from an ordinary array: node's
7162            // `arguments` is an exotic, and without the mark
7163            // `Array.isArray(arguments)` was true, the brand was
7164            // `[object Array]` and `util.types.isArgumentsObject` was false.
7165            // The backing representation stays an Array, which is what keeps
7166            // indices, `length`, spread and `for-of` working.
7167            h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7168            // `callee` is the function itself in SLOPPY code (it is a poison
7169            // pill only in strict, which the read path handles). It read back
7170            // `undefined`, so the pre-`class` self-reference idiom
7171            // `(function(){ arguments.callee })` found nothing.
7172            if let Some(f) = callee {
7173                if sloppy {
7174                    h.set_fn_prop(&a, "@@callee", f.clone());
7175                }
7176            }
7177            a
7178        });
7179        vars.entry("arguments".to_string()).or_insert(args_arr);
7180    }
7181    env.borrow_mut().vars = vars;
7182}
7183
7184/// Construct an instance with `new` — creates a fresh object, binds it as
7185/// `this`, runs the constructor, and returns the object (unless the constructor
7186/// returns its own object).
7187pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7188    construct_nt(ctor, args, ctor.clone())
7189}
7190
7191/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7192/// calls `super(...)` — the target stays the originally-`new`ed class).
7193pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7194    // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7195    if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7196        return crate::proxy::construct(ctor, args, &new_target)
7197            .map(|r| r.expect("kind_of said Proxy"));
7198    }
7199    let obj = with_host(|h| h.get(ctor).cloned());
7200    match obj {
7201        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7202        Some(JsObj::Func(fv)) => {
7203            // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7204            // `function*` and an `async function` are callable but not
7205            // constructable (10.2.2 is installed only for the ordinary case), so
7206            // `new` on one is a TypeError — node-js instead ran the body and
7207            // handed back a half-built instance (for a generator, an object whose
7208            // constructor had returned a suspended generator).
7209            let non_ctor = with_host(|h| {
7210                h.funcs
7211                    .get(fv.def_id)
7212                    // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7213                    // is `TypeError: o.m is not a constructor` on node v26.7.0,
7214                    // which is also why a method owns no `prototype`.
7215                    .map(|d| d.is_generator || d.is_async || d.is_method)
7216                    .unwrap_or(false)
7217            });
7218            if fv.is_arrow || non_ctor {
7219                return Err(not_a_constructor(ctor));
7220            }
7221            // A plain constructor function: instance delegates to `fn.prototype`
7222            // (auto-created with a `.constructor` back-link if not yet accessed).
7223            let inst = with_host(|h| {
7224                let o = h.new_object(IndexMap::new());
7225                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7226                    let p = h.new_object(IndexMap::new());
7227                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7228                        pp.insert("constructor".to_string(), ctor.clone());
7229                    }
7230                    // `F.prototype.constructor` is non-enumerable in JS.
7231                    h.hide_prop(&p, "constructor");
7232                    h.set_fn_prop(ctor, "prototype", p.clone());
7233                    p
7234                });
7235                h.set_proto(&o, proto);
7236                o
7237            });
7238            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7239            if returns_object(&r) {
7240                Ok(r)
7241            } else {
7242                Ok(inst)
7243            }
7244        }
7245        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7246        Some(JsObj::BoundFunc {
7247            target, args: pre, ..
7248        }) => {
7249            let mut all = pre;
7250            all.extend(args);
7251            construct_nt(&target, all, new_target)
7252        }
7253        _ => Err(not_a_constructor(ctor)),
7254    }
7255}
7256
7257/// `TypeError: <callee> is not a constructor`.
7258///
7259/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7260/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7261/// its name — the same string in the common case — and anything else by its
7262/// value.
7263fn not_a_constructor(ctor: &Value) -> String {
7264    let name = with_host(|h| match h.callable_name(ctor) {
7265        n if n.is_empty() => h.str_of(ctor),
7266        n => n,
7267    });
7268    type_error(&format!("{name} is not a constructor"))
7269}
7270
7271/// Whether a constructor's return value is an object (so `new` yields it instead
7272/// of the fresh instance). In JS "object" includes functions — the `router`
7273/// package's constructor `return router` (a function) must be honored, or the
7274/// returned router loses its callable identity.
7275fn returns_object(r: &Value) -> bool {
7276    matches!(
7277        with_host(|h| h.get(r).cloned()),
7278        Some(JsObj::Object(_))
7279            | Some(JsObj::Array(_))
7280            | Some(JsObj::Map { .. })
7281            | Some(JsObj::Set { .. })
7282            | Some(JsObj::Func(_))
7283            | Some(JsObj::Class(_))
7284            | Some(JsObj::BoundFunc { .. })
7285            | Some(JsObj::BoundMethod { .. })
7286            | Some(JsObj::RegExp(_))
7287    )
7288}
7289
7290/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7291/// run field initializers + the constructor (which may call `super(...)`).
7292fn construct_class(
7293    class_val: &Value,
7294    args: Vec<Value>,
7295    new_target: Value,
7296) -> Result<Value, String> {
7297    let cv = match with_host(|h| h.get(class_val).cloned()) {
7298        Some(JsObj::Class(c)) => c,
7299        _ => return Err(type_error("not a class")),
7300    };
7301    // Resolve the prototype of the *most-derived* class being `new`ed, so an
7302    // instance created through a `super()` chain still delegates to the leaf
7303    // prototype (correct method resolution).
7304    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7305        Some(JsObj::Class(c)) => c.proto.clone(),
7306        _ => cv.proto.clone(),
7307    };
7308    let inst = with_host(|h| {
7309        let o = h.new_object(IndexMap::new());
7310        h.set_proto(&o, leaf_proto.clone());
7311        o
7312    });
7313    // A `super()` deeper in may substitute the instance; the previous value is
7314    // restored so a `new` inside a constructor body cannot be mistaken for one.
7315    let saved = with_host(|h| h.swap_super_replacement(None));
7316    let ran = run_class_ctor(&cv, &inst, args, &new_target);
7317    let substituted = with_host(|h| {
7318        let s = h.take_super_replacement();
7319        h.swap_super_replacement(saved);
7320        s
7321    });
7322    // A constructor that returns an object replaces the instance (`new`
7323    // semantics); failing that, whatever `super()` substituted for it.
7324    match ran? {
7325        Some(obj) if returns_object(&obj) => Ok(obj),
7326        _ => Ok(substituted.unwrap_or(inst)),
7327    }
7328}
7329
7330/// Run one class's field initializers then its constructor on an existing
7331/// instance. Returns the constructor's explicit object return (if any). For a
7332/// base class this is the whole init; for a derived class the constructor body
7333/// reaches `super(...)` which recurses into the parent.
7334fn run_class_ctor(
7335    cv: &ClassVal,
7336    inst: &Value,
7337    args: Vec<Value>,
7338    new_target: &Value,
7339) -> Result<Option<Value>, String> {
7340    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7341    // that. A base class initializes fields before the constructor body.
7342    if cv.parent.is_none() {
7343        init_fields(cv, inst)?;
7344    }
7345    match &cv.ctor {
7346        Some(ctor_fn) => {
7347            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7348                Some(JsObj::Func(f)) => f,
7349                _ => return Err(type_error("class constructor is not a function")),
7350            };
7351            if cv.parent.is_some() {
7352                with_host(|h| h.mark_next_call_derived_ctor());
7353            }
7354            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7355            return Ok(Some(r));
7356        }
7357        None => {
7358            // Default constructor: `constructor(...a){ super(...a); }` for a
7359            // derived class, empty for a base class.
7360            if let Some(parent) = &cv.parent {
7361                // A base constructor's returned object becomes the instance, so
7362                // the implicit `constructor(...a){ super(...a) }` hands it on.
7363                if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7364                    init_fields(cv, &replacement)?;
7365                    return Ok(Some(replacement));
7366                }
7367                init_fields(cv, inst)?;
7368            }
7369        }
7370    }
7371    Ok(None)
7372}
7373
7374/// Evaluate and assign a class's instance-field initializers on `inst`.
7375fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7376    for (name, thunk, name_anon) in &cv.fields {
7377        init_one_field(inst, name, thunk, *name_anon)?;
7378    }
7379    Ok(())
7380}
7381
7382/// Evaluate ONE instance-field initializer thunk and install the result on
7383/// `inst`.
7384///
7385/// Shared by the base-class path (`init_fields`) and the derived-class path
7386/// that runs after `super(...)`; the two used to be separate loops, and only the
7387/// first canonicalized an array-index key.
7388///
7389/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7390/// gives the function the name `f`. It is decided by the compiler from the
7391/// syntax, never from the value.
7392pub fn init_one_field(
7393    inst: &Value,
7394    name: &str,
7395    thunk: &Value,
7396    name_anon: bool,
7397) -> Result<(), String> {
7398    // The thunk is an arrow capturing the class scope; run it with `this`=inst
7399    // so `this.other`-referencing initializers work.
7400    let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7401    with_host(|h| {
7402        if name_anon {
7403            let s = h.new_str(name.to_string());
7404            h.set_fn_prop(&val, "name", s);
7405        }
7406        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7407            let is_new = !props.contains_key(name);
7408            props.insert(name.to_string(), val);
7409            if is_new && array_index(name).is_some() {
7410                canonicalize_own_keys(props);
7411            }
7412        }
7413    });
7414    Ok(())
7415}
7416
7417/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7418/// kind (class vs plain function vs builtin) using the existing instance.
7419/// Run the parent constructor against `inst`.
7420///
7421/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7422/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7423/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7424/// derived constructor writes to the object `new` will hand back.
7425pub fn super_construct(
7426    parent: &Value,
7427    args: Vec<Value>,
7428    inst: &Value,
7429    new_target: &Value,
7430) -> Result<Option<Value>, String> {
7431    match with_host(|h| h.get(parent).cloned()) {
7432        Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7433            .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7434        Some(JsObj::Func(fv)) => {
7435            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7436            Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7437        }
7438        Some(JsObj::Builtin(name)) => {
7439            let built = crate::builtins::construct_builtin(&name, args)?;
7440            // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7441            // the heap variant, not in a property map, so copying own props
7442            // cannot carry it: the instance has to BECOME the built object.
7443            // Without this `new (class extends Array {})().push` was not a
7444            // function, and the same for Map, Set, RegExp, Promise and
7445            // Function — subclassing a builtin produced a plain object.
7446            if !become_exotic(inst, &built) {
7447                // An `Error` subclass is ordinary: its state IS own properties.
7448                adopt_own_props(inst, &built);
7449            }
7450            Ok(None)
7451        }
7452        // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7453        // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7454        // to the target). node-js initializes an ALREADY-allocated `inst` rather
7455        // than adopting the constructor's return value, so what the proxy built
7456        // is moved across — the same move the builtin arm makes.
7457        Some(JsObj::Proxy { .. }) => {
7458            let built = construct_nt(parent, args, new_target.clone())?;
7459            if !become_exotic(inst, &built) {
7460                adopt_own_props(inst, &built);
7461            }
7462            Ok(None)
7463        }
7464        _ => Err(type_error("super is not a constructor")),
7465    }
7466}
7467
7468/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7469/// a parent constructor produces a fresh object but node-js's class model has
7470/// already allocated the instance `this` is bound to.
7471/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7472/// extends a builtin EXOTIC really is one.
7473///
7474/// `inst` keeps its identity and its prototype link — the leaf class's
7475/// prototype, which is what method resolution and `instanceof` walk — while its
7476/// contents become the exotic the parent constructor produced. The side tables
7477/// keyed by heap index (array holes, property attributes, the fn-prop table)
7478/// move across with it.
7479///
7480/// Returns false for a variant whose state is ordinary own properties
7481/// (`Error`), which the caller copies instead.
7482fn become_exotic(inst: &Value, built: &Value) -> bool {
7483    let exotic = matches!(
7484        with_host(|h| h.get(built).cloned()),
7485        Some(JsObj::Array(_))
7486            | Some(JsObj::Map { .. })
7487            | Some(JsObj::Set { .. })
7488            | Some(JsObj::RegExp(_))
7489            | Some(JsObj::Promise { .. })
7490            | Some(JsObj::Func(_))
7491            | Some(JsObj::Str(_))
7492            | Some(JsObj::BigInt(_))
7493            | Some(JsObj::Symbol { .. })
7494    );
7495    if !exotic {
7496        return false;
7497    }
7498    let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7499        return false;
7500    };
7501    let (dst, src) = (*dst, *src);
7502    with_host(|h| {
7503        if let Some(obj) = h.get(built).cloned() {
7504            if let Some(slot) = h.get_mut(inst) {
7505                *slot = obj;
7506            }
7507        }
7508        h.move_index_state(src, dst);
7509    });
7510    true
7511}
7512
7513fn adopt_own_props(inst: &Value, built: &Value) {
7514    let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7515        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7516        _ => Vec::new(),
7517    });
7518    with_host(|h| {
7519        let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7520        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7521            for (k, v) in entries {
7522                props.insert(k, v);
7523            }
7524            canonicalize_own_keys(props);
7525        }
7526        // The copied slots keep the attributes the source gave them, so
7527        // `class E extends Error` instances hide `message`/`stack` too.
7528        for k in keys {
7529            let a = h.prop_attrs(built, &k);
7530            h.set_prop_attrs(inst, &k, a);
7531        }
7532    });
7533}
7534
7535// ── class construction (runtime) ─────────────────────────────────────────────
7536
7537/// Build a class constructor value from its parts. The compiler emits (via
7538/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7539/// undefined for a default constructor); methods/getters/setters/statics/fields
7540/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7541pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7542    // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7543    // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7544    // trap and so re-enters the host, which the borrow below cannot allow.
7545    // Without it the link fell back to `Object.prototype` and every inherited
7546    // method went missing.
7547    let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7548        .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7549        .flatten();
7550    with_host(|h| {
7551        let parent_opt = if matches!(parent, Value::Undef) {
7552            None
7553        } else {
7554            Some(parent.clone())
7555        };
7556        // The class prototype delegates to the parent's prototype (or
7557        // Object.prototype for a base class). Extending a builtin error links to
7558        // that error's prototype so `instanceof Error` holds for the subclass.
7559        let parent_proto = match &parent_opt {
7560            Some(_) if proxy_parent_proto.is_some() => {
7561                proxy_parent_proto.clone().expect("checked is_some")
7562            }
7563            Some(p) => match h.get(p).cloned() {
7564                Some(JsObj::Class(pc)) => pc.proto.clone(),
7565                Some(JsObj::Builtin(bn)) => {
7566                    h.ensure_error_protos();
7567                    h.ensure_native_protos();
7568                    // `class S extends String {}` links to the REAL
7569                    // `String.prototype`, the same way an error subclass links
7570                    // to its error prototype. Without it `S.prototype`'s
7571                    // `[[Prototype]]` fell back to `Object.prototype`, so
7572                    // `new S("hi") instanceof String` read false and
7573                    // `String(new S("hi"))` reported `[object String]` instead
7574                    // of `hi`.
7575                    error_proto_of(h, &bn)
7576                        .or_else(|| h.native_proto(&bn))
7577                        .or_else(|| h.fn_prop(p, "prototype"))
7578                        .unwrap_or_else(|| h.object_proto())
7579                }
7580                _ => h
7581                    .fn_prop(p, "prototype")
7582                    .unwrap_or_else(|| h.object_proto()),
7583            },
7584            None => h.object_proto(),
7585        };
7586        let proto = h.new_object(IndexMap::new());
7587        h.set_proto(&proto, parent_proto);
7588        let ctor_opt = if matches!(ctor, Value::Undef) {
7589            None
7590        } else {
7591            Some(ctor.clone())
7592        };
7593        // Give the constructor closure its home class (for `super.method()`), and
7594        // record its `.name`.
7595        if let Some(cf) = &ctor_opt {
7596            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7597                f.home_class = Some(name.to_string());
7598            }
7599        }
7600        let cval = ClassVal {
7601            name: name.to_string(),
7602            ctor: ctor_opt,
7603            parent: parent_opt,
7604            proto: proto.clone(),
7605            statics: IndexMap::new(),
7606            fields: Vec::new(),
7607            source_def,
7608        };
7609        let class_val = h.alloc(JsObj::Class(cval));
7610        h.class_registry.insert(name.to_string(), class_val.clone());
7611        // Link prototype → class (for instance display + `constructor`), and give
7612        // the class its own `prototype` fn-prop so `C.prototype` reads work.
7613        h.tag_proto_class(&proto, class_val.clone());
7614        h.set_fn_prop(&class_val, "prototype", proto.clone());
7615        // `Class.prototype.constructor === Class`.
7616        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7617            p.insert("constructor".to_string(), class_val.clone());
7618        }
7619        h.hide_prop(&proto, "constructor");
7620        class_val
7621    })
7622}
7623
7624/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7625/// `member::*` tag; `is_static` targets the constructor side.
7626pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7627    with_host(|h| {
7628        let cname = match h.get(class_val) {
7629            Some(JsObj::Class(c)) => c.name.clone(),
7630            _ => String::new(),
7631        };
7632        // A private method/accessor: remember which class declared it, so a
7633        // brand-check failure can name the class the way node does. A static
7634        // FIELD is data, not a method, so it keeps the field wording.
7635        if name.starts_with('#') && kind != member::STATIC_FIELD {
7636            h.note_private_method(name);
7637        }
7638        // Give the method its home class for `super.x()`, and record whether it
7639        // is static — `super` resolves against a different object either way.
7640        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7641            f.home_class = Some(cname);
7642            f.home_static = is_static;
7643        }
7644        // Static members live on the constructor (fn-props / static accessors);
7645        // instance members on the prototype.
7646        let target = if is_static {
7647            class_val.clone()
7648        } else {
7649            match h.get(class_val) {
7650                Some(JsObj::Class(c)) => c.proto.clone(),
7651                _ => return,
7652            }
7653        };
7654        match kind {
7655            member::GET => h.set_accessor(&target, name, Some(func), None),
7656            member::SET => h.set_accessor(&target, name, None, Some(func)),
7657            _ => {
7658                // A static field is enumerable (`Object.keys(C)` lists it) unlike
7659                // a method, so it must not reach the `hide_prop` below.
7660                if kind == member::STATIC_FIELD {
7661                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7662                        c.statics.insert(name.to_string(), func.clone());
7663                    }
7664                    h.set_fn_prop(class_val, name, func);
7665                    return;
7666                }
7667                if is_static {
7668                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7669                        c.statics.insert(name.to_string(), func.clone());
7670                    }
7671                    h.set_fn_prop(class_val, name, func);
7672                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7673                    p.insert(name.to_string(), func);
7674                }
7675            }
7676        }
7677        // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7678        // Evaluation), so `for (k in instance)` walking the prototype chain never
7679        // yields them and `Object.keys(C.prototype)` is empty.
7680        h.hide_prop(&target, name);
7681    });
7682}
7683
7684/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7685pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7686    with_host(|h| {
7687        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7688            c.fields.push((name.to_string(), thunk, name_anon));
7689        }
7690    });
7691}
7692
7693/// The `[[Prototype]]` object a constructor value hands to its instances
7694/// (`Ctor.prototype`), for `instanceof`.
7695fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7696    match h.get(ctor) {
7697        Some(JsObj::Class(c)) => Some(c.proto.clone()),
7698        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7699        // A builtin's prototype lives in one of two registries: the error
7700        // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7701        // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7702        // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7703        // `Buffer.prototype instanceof Uint8Array` read false even though the
7704        // link was there — the instance case only passed via a native-tag
7705        // special case, which a prototype object does not carry.
7706        Some(JsObj::Builtin(name)) => h
7707            .error_protos
7708            .get(name)
7709            .or_else(|| h.native_protos.get(name))
7710            .cloned(),
7711        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7712        _ => None,
7713    }
7714}
7715
7716/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7717/// so a chain walk driven by that function can compare the two with `strict_eq`.
7718///
7719/// `ctor_prototype` answers only for the constructors whose prototype object
7720/// really exists on the heap (classes, user functions, the error and native
7721/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7722/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7723/// mirrors that fallback rather than reporting "no prototype" and failing every
7724/// comparison.
7725fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7726    if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7727        return Some(p);
7728    }
7729    let name = with_host(|h| match h.get(ctor) {
7730        Some(JsObj::Builtin(n)) => Some(n.clone()),
7731        _ => None,
7732    })?;
7733    if name == "Object" {
7734        return Some(with_host(|h| h.object_proto()));
7735    }
7736    Some(with_host(|h| {
7737        h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7738    }))
7739}
7740
7741/// V8's "not a function" wording for a value that was expected to be callable.
7742/// A number/string/boolean is named WITH its value (`number 1 is not a
7743/// function`, `string "s" is not a function`); every other type is named by type
7744/// alone (`object is not a function`, `symbol is not a function`).
7745pub fn not_a_function_message(v: &Value) -> String {
7746    with_host(|h| match v {
7747        Value::Undef => "undefined is not a function".into(),
7748        Value::Bool(b) => format!("boolean {b} is not a function"),
7749        Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7750        Value::Str(s) => format!("string \"{s}\" is not a function"),
7751        Value::Obj(_) => match h.get(v) {
7752            Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7753            Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7754            Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7755            _ => "object is not a function".into(),
7756        },
7757        _ => "object is not a function".into(),
7758    })
7759}
7760
7761/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7762/// `ctor.prototype`.
7763pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7764    // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7765    // right-hand side REPLACES the prototype-chain walk entirely, and it is
7766    // consulted before the callability check — which is why a plain (uncallable)
7767    // object that defines it is a legal `instanceof` right-hand side.
7768    if matches!(ctor, Value::Obj(_)) {
7769        // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7770        // plain function both land in the fn-prop side table (which
7771        // `class_static` reads, following the `extends` chain), NOT in an object
7772        // property map — so consulting only `lookup_chain` would find the object
7773        // literal form and silently miss the two forms V8 users actually write.
7774        let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7775            Some(f) => Some(f),
7776            None => protocol_lookup(ctor, "@@hasInstance")?,
7777        };
7778        // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7779        // else that is not callable is a TypeError, so a data property here does
7780        // NOT fall back to the prototype walk.
7781        match handler {
7782            Some(f) if with_host(|h| is_callable(h, &f)) => {
7783                let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7784                return Ok(with_host(|h| h.truthy(&r)));
7785            }
7786            Some(f)
7787                if !matches!(f, Value::Undef)
7788                    && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7789            {
7790                return Err(type_error(&not_a_function_message(&f)));
7791            }
7792            _ => {}
7793        }
7794    }
7795    // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7796    // `1 instanceof 3` throws even though the left side could never match.
7797    // Returning early on the left side skipped that check entirely.
7798    let ctor_callable = with_host(|h| {
7799        matches!(
7800            h.get(ctor),
7801            Some(JsObj::Func(_))
7802                | Some(JsObj::Class(_))
7803                | Some(JsObj::Builtin(_))
7804                | Some(JsObj::BoundFunc { .. })
7805        )
7806    });
7807    if !ctor_callable {
7808        // V8 has TWO messages here and they are not interchangeable: a primitive
7809        // right-hand side is "not an object", an object that is merely not
7810        // callable is "not callable". Only the second was implemented, so
7811        // `1 instanceof 3` reported nothing at all.
7812        return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7813            "Right-hand side of 'instanceof' is not callable"
7814        } else {
7815            "Right-hand side of 'instanceof' is not an object"
7816        }));
7817    }
7818    // A non-object left-hand side is never an instance — but only after the
7819    // right-hand side has been validated above.
7820    if !matches!(obj, Value::Obj(_)) {
7821        return Ok(false);
7822    }
7823    // A Proxy shares no heap variant with its target, so the structural arms
7824    // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7825    // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7826    // walk here, which also gives a custom trap the final say.
7827    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7828        with_host(|h| {
7829            h.ensure_error_protos();
7830            h.ensure_native_protos();
7831        });
7832        let Some(target) = walk_target_prototype(ctor) else {
7833            return Ok(false);
7834        };
7835        let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7836        for _ in 0..100 {
7837            if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7838                return Ok(false);
7839            }
7840            if with_host(|h| h.strict_eq(&cur, &target)) {
7841                return Ok(true);
7842            }
7843            cur = crate::builtins::prototype_of(&cur);
7844        }
7845        return Ok(false);
7846    }
7847    // Builtin constructors whose instances aren't prototype-linked in our model
7848    // (arrays/plain objects/functions) get a structural instanceof.
7849    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7850        // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7851        // an instance regardless of its own kind, which is the whole of the ES5
7852        // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7853        // makes `new F() instanceof Array` true. A structural test alone said
7854        // false.
7855        if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7856            return Ok(true);
7857        }
7858        let kind = with_host(|h| h.get(obj).cloned());
7859        match name.as_str() {
7860            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7861            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7862            // Map/Set/Promise instances are distinct heap variants, not
7863            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7864            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7865            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7866            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7867            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7868            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7869            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7870            // A RegExp is its own heap variant too, not a prototype-linked object.
7871            "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7872            "Object" => {
7873                // Everything object-typed except a null-prototype object is an
7874                // Object instance.
7875                let is_obj = matches!(
7876                    kind,
7877                    Some(JsObj::Object(_))
7878                        | Some(JsObj::Array(_))
7879                        // A namespace object and a builtin function are both
7880                        // `instanceof Object`: `Math instanceof Object` is true.
7881                        | Some(JsObj::Builtin(_))
7882                        | Some(JsObj::Func(_))
7883                        | Some(JsObj::Class(_))
7884                        | Some(JsObj::Map { .. })
7885                        | Some(JsObj::Set { .. })
7886                        | Some(JsObj::Promise { .. })
7887                        | Some(JsObj::Generator { .. })
7888                        | Some(JsObj::RegExp(_))
7889                );
7890                if is_obj {
7891                    // A null-prototype object (Object.create(null) or
7892                    // setPrototypeOf(o, null)) is NOT an Object instance.
7893                    if with_host(|h| h.has_null_proto(obj)) {
7894                        return Ok(false);
7895                    }
7896                    return Ok(true);
7897                }
7898                return Ok(false);
7899            }
7900            // A Node `Buffer` IS a `Uint8Array` subclass instance.
7901            "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7902                return Ok(true);
7903            }
7904            // Every typed array carries the same `TypedArray` tag; the constructor
7905            // it is an instance of is its ELEMENT KIND.
7906            k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7907                return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7908            }
7909            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7910            // `TextEncoder`, …) is an instance of the builtin whose name matches
7911            // its hidden `@@native` tag.
7912            other => {
7913                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7914                    return Ok(true);
7915                }
7916            }
7917        }
7918    }
7919    with_host(|h| h.ensure_error_protos());
7920    // The native exotic prototypes are built lazily; `instanceof` may be the
7921    // first thing to ask for them, so materialise them before the chain walk.
7922    with_host(|h| h.ensure_native_protos());
7923    let target = match with_host(|h| ctor_prototype(h, ctor)) {
7924        Some(p) => p,
7925        None => return Ok(false),
7926    };
7927    let mut cur = with_host(|h| h.proto_of(obj));
7928    while let Some(p) = cur {
7929        if with_host(|h| h.strict_eq(&p, &target)) {
7930            return Ok(true);
7931        }
7932        cur = with_host(|h| h.proto_of(&p));
7933    }
7934    Ok(false)
7935}
7936
7937// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7938
7939impl JsHost {
7940    /// Swap the volatile execution context in one shot, returning the previous
7941    /// one — installs a generator's context on resume, pulls it back on suspend.
7942    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7943        std::mem::swap(&mut self.frames, &mut c.frames);
7944        std::mem::swap(&mut self.error, &mut c.error);
7945        std::mem::swap(&mut self.exc, &mut c.exc);
7946        std::mem::swap(&mut self.signal, &mut c.signal);
7947        c
7948    }
7949    pub fn is_generator_val(&self, v: &Value) -> bool {
7950        matches!(self.get(v), Some(JsObj::Generator { .. }))
7951    }
7952    /// Whether `v` is an ASYNC generator object — the borrow-free form of
7953    /// [`is_async_generator`], usable from code already holding the host.
7954    pub fn is_async_gen_val(&self, v: &Value) -> bool {
7955        match self.get(v) {
7956            Some(JsObj::Generator { id }) => self
7957                .generators
7958                .get(*id as usize)
7959                .map(|g| g.async_gen)
7960                .unwrap_or(false),
7961            _ => false,
7962        }
7963    }
7964    pub fn gen_done(&self, id: u32) -> bool {
7965        self.generators
7966            .get(id as usize)
7967            .map(|g| g.done)
7968            .unwrap_or(true)
7969    }
7970    fn gen_started(&self, id: u32) -> bool {
7971        self.generators
7972            .get(id as usize)
7973            .map(|g| g.started)
7974            .unwrap_or(false)
7975    }
7976}
7977
7978/// Build a suspended generator whose body is `chunk`, run in a frame with the
7979/// already-bound `env`. Nothing executes until the first `gen_resume`.
7980fn make_generator(
7981    chunk: Chunk,
7982    env: Env,
7983    this_val: Option<Value>,
7984    home_class: Option<String>,
7985    home_static: bool,
7986    home_object: Option<Value>,
7987    strict: bool,
7988) -> Value {
7989    let home = home_class
7990        .as_ref()
7991        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7992    let frame = Frame {
7993        base_env: env.clone(),
7994        env,
7995        this_obj: this_val,
7996        new_target: None,
7997        home_class: home,
7998        home_static,
7999        home_object,
8000        strict,
8001        line: 0,
8002        owner: None,
8003        is_module: false,
8004        this_state: ThisState::Plain,
8005    };
8006    let id = with_host(|h| {
8007        let id = h.generators.len() as u32;
8008        h.generators.push(GenCell {
8009            coro: None,
8010            yielder: std::ptr::null(),
8011            ctx: GenContext {
8012                frames: vec![frame],
8013                ..GenContext::default()
8014            },
8015            done: false,
8016            started: false,
8017            inject: None,
8018            async_gen: false,
8019            queue: std::collections::VecDeque::new(),
8020            running: false,
8021            stack_floor: 0,
8022        });
8023        id
8024    });
8025    let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
8026        ensure_coroutine_floor();
8027        // Same thread → publish the yielder so `yield` (deep in the body's VM)
8028        // can reach it. Valid for the whole body lifetime.
8029        with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
8030        let r = run_chunk_on(chunk);
8031        // A `return` inside the body leaves a Return signal carrying the final
8032        // value; capture it so `.next()` reports it as the completion value.
8033        let ret = with_host(|h| match h.signal.take() {
8034            Some(Signal::Return(v)) => v,
8035            _ => Value::Undef,
8036        });
8037        r.map(|_| ret)
8038    };
8039    // The body's stack is allocated here rather than left to `Coroutine::new` so
8040    // that its size is ours to choose and, above all, so its `limit()` is known:
8041    // that address is what `stack_exhausted` must compare against while the body
8042    // runs, since a coroutine does NOT run on the thread stack pthread reports.
8043    // A refused reservation still yields a working generator on corosensei's own
8044    // 1 MiB default, with a floor derived on entry instead.
8045    let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
8046        Ok(stack) => {
8047            let floor = coro_stack_floor(&stack);
8048            (corosensei::Coroutine::with_stack(stack, body), floor)
8049        }
8050        Err(_) => (corosensei::Coroutine::new(body), 0),
8051    };
8052    with_host(|h| {
8053        h.generators[id as usize].coro = Some(coro);
8054        h.generators[id as usize].stack_floor = floor;
8055    });
8056    with_host(|h| h.alloc(JsObj::Generator { id }))
8057}
8058
8059/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
8060/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
8061pub fn gen_yield(v: Value) -> Result<Value, String> {
8062    let id = match CUR_GEN.with(|c| c.get()) {
8063        Some(id) => id,
8064        None => return Err(type_error("yield outside a generator")),
8065    };
8066    let yp = with_host(|h| h.generators[id as usize].yielder);
8067    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
8068    // only reach here from inside that body (its stack is live).
8069    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
8070    let sent = yielder.suspend(v);
8071    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
8072    // convert it into a Return signal / thrown value so the body unwinds and any
8073    // `finally` runs, exactly as a source-level `return`/`throw` would.
8074    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
8075        match inj {
8076            GenInject::Return(rv) => {
8077                with_host(|h| h.signal = Some(Signal::Return(rv)));
8078                return Ok(Value::Undef);
8079            }
8080            GenInject::Throw(ev) => {
8081                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
8082                with_host(|h| h.exc = Some(ev));
8083                return Err(msg);
8084            }
8085        }
8086    }
8087    Ok(sent)
8088}
8089
8090/// `generator.return(v)`: force the generator to complete, running any pending
8091/// `finally`. If it is already done (or never started) it just reports
8092/// `{value:v, done:true}` without executing the body.
8093pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
8094    let id = match with_host(|h| h.get(gen).cloned()) {
8095        Some(JsObj::Generator { id }) => id,
8096        _ => return Err(type_error("not a generator")),
8097    };
8098    // Not started yet (coro present, ctx never resumed) OR already done → no body
8099    // to unwind: complete immediately with the supplied value.
8100    let started = with_host(|h| h.gen_started(id));
8101    if with_host(|h| h.generators[id as usize].done) || !started {
8102        with_host(|h| h.generators[id as usize].done = true);
8103        return Ok(GenStep::Done(v));
8104    }
8105    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
8106    gen_resume(gen, Value::Undef)
8107}
8108
8109/// `generator.throw(e)`: inject a throw at the suspension point, running any
8110/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
8111pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
8112    let id = match with_host(|h| h.get(gen).cloned()) {
8113        Some(JsObj::Generator { id }) => id,
8114        _ => return Err(type_error("not a generator")),
8115    };
8116    let started = with_host(|h| h.gen_started(id));
8117    if with_host(|h| h.generators[id as usize].done) || !started {
8118        // A throw into a done/unstarted generator propagates to the caller.
8119        with_host(|h| h.generators[id as usize].done = true);
8120        let msg = with_host(|h| crate::builtins::error_string(h, &e));
8121        with_host(|h| h.exc = Some(e));
8122        return Err(msg);
8123    }
8124    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
8125    gen_resume(gen, Value::Undef)
8126}
8127
8128/// Outcome of resuming a generator: a yielded value (not done), or the final
8129/// completion value (done).
8130pub enum GenStep {
8131    Yield(Value),
8132    Done(Value),
8133}
8134
8135/// Resume a generator until its next `yield` or its body returns. Preserves the
8136/// shared host: the coroutine is taken out so the body re-enters `with_host`
8137/// freely, and the volatile context is swapped so the caller's frames/signal
8138/// survive the switch.
8139pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8140    let id = match with_host(|h| h.get(gen).cloned()) {
8141        Some(JsObj::Generator { id }) => id,
8142        _ => return Err(type_error("not a generator")),
8143    };
8144    if with_host(|h| h.generators[id as usize].done) {
8145        return Ok(GenStep::Done(Value::Undef));
8146    }
8147    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8148        Some(c) => c,
8149        None => return Err("TypeError: generator already executing".into()),
8150    };
8151    with_host(|h| h.generators[id as usize].started = true);
8152    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8153    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8154    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8155    // The body runs on the coroutine's OWN stack, so the guard's floor has to
8156    // move with it and move back on suspend — generators nest, and a resume from
8157    // inside another generator must restore that one's floor, not the thread's.
8158    let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8159    let caller_floor = swap_stack_floor(coro_floor);
8160
8161    let out = coro.resume(send); // no host borrow held; body drives its own VM
8162
8163    let measured = swap_stack_floor(caller_floor);
8164    // A coroutine on corosensei's default stack has no known bounds, so the
8165    // floor it measured for itself on first entry is kept for later resumes.
8166    if coro_floor == 0 && measured != 0 {
8167        with_host(|h| h.generators[id as usize].stack_floor = measured);
8168    }
8169    CUR_GEN.with(|c| c.set(prev));
8170    let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8171    // A `throw` inside the body left the thrown VALUE in the generator's context,
8172    // which the swap above just stashed away. Hand it to the caller so the
8173    // rejection/catch keeps the original error object instead of a string rebuild.
8174    let thrown = gen_ctx.exc.take();
8175    with_host(|h| {
8176        if let Some(v) = thrown {
8177            h.exc = Some(v);
8178        }
8179        h.generators[id as usize].ctx = gen_ctx;
8180        h.generators[id as usize].coro = Some(coro);
8181    });
8182
8183    match out {
8184        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8185        corosensei::CoroutineResult::Return(r) => {
8186            // Release the coroutine — and with it the mmap'd stack it owns —
8187            // the moment the body completes. `h.generators` only ever grows (an
8188            // id is never reused), so a program that awaits in a loop otherwise
8189            // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8190            // the life of the process. A finished generator is never resumed:
8191            // `gen_resume` returns `Done` on the `done` flag before it looks.
8192            with_host(|h| {
8193                let g = &mut h.generators[id as usize];
8194                g.done = true;
8195                g.coro = None;
8196            });
8197            match r {
8198                Ok(v) => Ok(GenStep::Done(v)),
8199                Err(e) => Err(e),
8200            }
8201        }
8202    }
8203}
8204
8205/// Force a generator to completion (used by `.return()` and abandoned loops):
8206/// marks it done without running further.
8207pub fn gen_close(gen: &Value) {
8208    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8209        with_host(|h| h.generators[id as usize].done = true);
8210    }
8211}
8212
8213// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8214
8215/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8216pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8217    match v {
8218        Value::Undef => MapKey::Undef,
8219        Value::Bool(b) => MapKey::Bool(*b),
8220        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8221        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8222        Value::Str(s) => MapKey::Str((**s).clone()),
8223        Value::Obj(i) => match h.get(v) {
8224            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8225            Some(JsObj::Null) => MapKey::Null,
8226            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8227            Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8228            _ => MapKey::Ref(*i),
8229        },
8230        _ => MapKey::Undef,
8231    }
8232}
8233
8234/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8235fn norm_num_bits(f: f64) -> u64 {
8236    if f.is_nan() {
8237        return f64::NAN.to_bits();
8238    }
8239    if f == 0.0 {
8240        return 0.0f64.to_bits(); // fold -0 into +0
8241    }
8242    f.to_bits()
8243}
8244
8245/// Fully materialize any iterable into a vector of values.
8246/// Pull at most `n` values, then close the iterator — 8.6.2
8247/// IteratorBindingInitialization, which is what an array destructuring pattern
8248/// without a `...rest` element performs.
8249///
8250/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8251/// fixed number of targets, so the spec pulls exactly that many and calls
8252/// IteratorClose on whatever is left; draining instead made
8253///
8254/// ```text
8255/// const [first] = infiniteGenerator();
8256/// ```
8257///
8258/// run forever. It is also observable on any finite iterator, as the count of
8259/// `next()` calls and whether `return()` ever ran.
8260///
8261/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8262/// using `iter_all` and an unbounded source hangs there in node too.
8263pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8264    // A Proxy iterates through its traps, which materialize eagerly; there is
8265    // no step-wise form to bound, so this keeps the draining behaviour.
8266    if let Some(items) = crate::proxy::iterate(v)? {
8267        return Ok(items.into_iter().take(n).collect());
8268    }
8269    if with_host(|h| h.is_generator_val(v)) {
8270        let mut out = Vec::new();
8271        while out.len() < n {
8272            match gen_resume(v, Value::Undef)? {
8273                GenStep::Yield(x) => out.push(x),
8274                _ => return Ok(out), // ran out on its own; nothing left to close
8275            }
8276        }
8277        // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8278        let _ = gen_return(v, Value::Undef);
8279        return Ok(out);
8280    }
8281    if let Some(iter_fn) = user_iterator_fn(v) {
8282        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8283        let mut out = Vec::new();
8284        while out.len() < n {
8285            let step = call_method(&iterator, "next", Vec::new())?;
8286            // Read first: resolving the property re-enters the host, so doing
8287            // it inside the `with_host` closure double-borrows and aborts.
8288            let done = get_prop_chain(&step, "done")?;
8289            if with_host(|h| h.truthy(&done)) {
8290                return Ok(out);
8291            }
8292            out.push(get_prop_chain(&step, "value")?);
8293        }
8294        // IteratorClose: `return` is optional on the protocol, and a throw from
8295        // it is swallowed here the way a normal (non-abrupt) completion does.
8296        if let Ok(ret) = get_prop_chain(&iterator, "return") {
8297            if with_host(|h| is_callable(h, &ret)) {
8298                let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8299            }
8300        }
8301        return Ok(out);
8302    }
8303    // Arrays, strings, Map/Set: already materialized, and their built-in
8304    // iterators carry no `return`, so there is nothing to close. The same
8305    // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8306    // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8307    // longer carried `Symbol.iterator`.
8308    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8309        let shown = with_host(|h| h.inspect(v));
8310        return Err(type_error(&format!("{shown} is not iterable")));
8311    }
8312    with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8313}
8314
8315pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8316    // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8317    // no heap variant `iter_vec` could recognise.
8318    if let Some(items) = crate::proxy::iterate(v)? {
8319        return Ok(items);
8320    }
8321    // Generators / user iterators must resume without a live host borrow.
8322    if with_host(|h| h.is_generator_val(v)) {
8323        let mut out = Vec::new();
8324        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8325            out.push(x);
8326        }
8327        return Ok(out);
8328    }
8329    // A live array iterator is stepped to the end, so an accessor or a hole
8330    // reads as `a[i]` does and the iterator is left exhausted, as spreading it
8331    // leaves it in node.
8332    if with_host(|h| matches!(h.get(v), Some(JsObj::Iter { array: Some(_), .. }))) {
8333        let mut out = Vec::new();
8334        while let Some(Some(x)) = crate::builtins::iter_step(v) {
8335            out.push(x);
8336        }
8337        return Ok(out);
8338    }
8339    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8340    // Checked BEFORE the reachability guard below, since an own `Symbol
8341    // .iterator` makes a value iterable no matter what its prototype is.
8342    if let Some(iter_fn) = user_iterator_fn(v) {
8343        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8344        return drain_iterator(&iterator);
8345    }
8346    // The fast paths below read a builtin's backing storage directly, which is
8347    // only legitimate while that builtin's `Symbol.iterator` is still
8348    // reachable: replacing the prototype takes it away, and node then reports
8349    // the value as not iterable. Spread, destructuring and `Array.from`'s
8350    // iterable branch all funnel through here.
8351    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8352        let shown = with_host(|h| h.inspect(v));
8353        return Err(type_error(&format!("{shown} is not iterable")));
8354    }
8355    // A String wrapper iterates its code POINTS, exactly as the primitive does
8356    // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8357    if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8358        if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8359            return iter_all(&prim);
8360        }
8361    }
8362    // An array's index ACCESSORS are not in its backing vector, so iterating one
8363    // (spread, `for-of`, `Array.from`) has to resolve them the way the
8364    // `Array.prototype` methods do.
8365    let mut items = with_host(|h| h.iter_vec(v))?;
8366    if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8367        crate::builtins::resolve_index_accessors_pub(v, &mut items);
8368    }
8369    Ok(items)
8370}
8371
8372// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8373
8374/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8375/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8376/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8377/// are awaited one at a time by `async_step`.
8378pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8379    if let Some(f) = user_async_iterator_fn(src) {
8380        return invoke(&f, Vec::new(), Some(src.clone()));
8381    }
8382    // An `async function*` object IS its own async iterator; draining it into a
8383    // list here would run the whole body (and any `finally`) before the consumer
8384    // sees the first value.
8385    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8386        if with_host(|h| h.generators[id as usize].async_gen) {
8387            return Ok(src.clone());
8388        }
8389    }
8390    let items = iter_all(src)?;
8391    Ok(with_host(|h| {
8392        h.alloc(JsObj::Iter {
8393            items,
8394            idx: 0,
8395            array: None,
8396        })
8397    }))
8398}
8399
8400/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8401fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8402    // A PROXY supplies the protocol through its `get` trap and is not a plain
8403    // object, so the shape test below rejects it outright.
8404    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8405        return protocol_lookup(v, "@@asyncIterator")
8406            .ok()
8407            .flatten()
8408            .filter(|f| with_host(|h| is_callable(h, f)));
8409    }
8410    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8411    if !is_plain {
8412        return None;
8413    }
8414    // Full property resolution, not a stored-property lookup — the same reason
8415    // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8416    // dispatches its methods through the stdlib method table rather than a
8417    // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8418    // even though reading it gives a function: `for await (const v of
8419    // timersPromises.setInterval(…))` said the iterator "is not iterable".
8420    let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8421    with_host(|h| is_callable(h, &f)).then_some(f)
8422}
8423
8424/// One step of a `for await` loop: return a Promise that settles to a
8425/// `{value, done}` record. For a native async iterator this is `iter.next()`
8426/// (already a promise of the record). For the sync fallback it pops the next raw
8427/// value, awaits it, and packages `{value: resolved, done:false}` (or
8428/// `{done:true}` at exhaustion).
8429pub fn async_step(iterator: &Value) -> Result<Value, String> {
8430    // An `async function*` object: resume it through the await-aware driver.
8431    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8432        if with_host(|h| h.generators[id as usize].async_gen) {
8433            return Ok(async_gen_step(iterator, Value::Undef));
8434        }
8435    }
8436    // Sync-fallback iterator: drive it here, awaiting each yielded value.
8437    if let Some(JsObj::Iter { items, idx, .. }) = with_host(|h| h.get(iterator).cloned()) {
8438        if idx >= items.len() {
8439            // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8440            // promise even at exhaustion, so the `done: true` step costs the same
8441            // two microtask ticks a value step does.
8442            let step = with_host(|h| h.new_promise());
8443            let sid = with_host(|h| h.promise_id(&step).unwrap());
8444            with_host(|h| {
8445                h.queue_micro_native(Box::new(move || {
8446                    resolve_promise_val(sid, iter_record(Value::Undef, true));
8447                    Ok(())
8448                }))
8449            });
8450            return Ok(step);
8451        }
8452        let raw = items[idx].clone();
8453        with_host(|h| {
8454            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8455                *idx += 1;
8456            }
8457        });
8458        // Await the raw value (adopts a promise's resolution), then wrap.
8459        let step = with_host(|h| h.new_promise());
8460        let sid = with_host(|h| h.promise_id(&step).unwrap());
8461        let raw_p = promise_of(&raw);
8462        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8463        subscribe_native(
8464            raw_id,
8465            Box::new(move |state, val| {
8466                if state == PromiseState::Rejected {
8467                    reject_promise_val(sid, val);
8468                } else {
8469                    resolve_promise_val(sid, iter_record(val, false));
8470                }
8471                Ok(())
8472            }),
8473        );
8474        return Ok(step);
8475    }
8476    // Native async iterator: `iter.next()` returns the {value,done} promise.
8477    let r = call_method(iterator, "next", Vec::new())?;
8478    Ok(promise_of(&r))
8479}
8480
8481/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8482/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8483pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8484    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8485    if !is_plain {
8486        return None;
8487    }
8488    // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8489    // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8490    // stdlib method table rather than a property map, so `lookup_chain` reported
8491    // no `Symbol.iterator` for one even though reading it gave a function —
8492    // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8493    let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8494    with_host(|h| is_callable(h, &f)).then_some(f)
8495}
8496
8497/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8498/// exhaustion.
8499/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8500/// `f` exits abruptly (7.4.9 IteratorClose).
8501///
8502/// The difference from `iter_all` + a loop is that this never materializes the
8503/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8504/// stop at the first call, and draining first means it never gets there at all.
8505pub fn iter_for_each(
8506    src: &Value,
8507    mut f: impl FnMut(Value, usize) -> Result<(), String>,
8508) -> Result<(), String> {
8509    // Only a USER iterator can be infinite or observe its own close; every
8510    // other shape is already a finite materialized sequence.
8511    let Some(iter_fn) = user_iterator_fn(src) else {
8512        for (i, v) in iter_all(src)?.into_iter().enumerate() {
8513            f(v, i)?;
8514        }
8515        return Ok(());
8516    };
8517    let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8518    let mut i = 0usize;
8519    loop {
8520        let step = call_method(&iterator, "next", Vec::new())?;
8521        let done = get_prop_chain(&step, "done")?;
8522        if with_host(|h| h.truthy(&done)) {
8523            return Ok(());
8524        }
8525        let value = get_prop_chain(&step, "value")?;
8526        if let Err(e) = f(value, i) {
8527            // The callback's error wins over anything `return()` raises, so a
8528            // throwing `return` is swallowed here (7.4.9 step 6).
8529            let _ = close_iterator(&iterator);
8530            return Err(e);
8531        }
8532        i += 1;
8533    }
8534}
8535
8536/// Call `iterator.return()` if it has one, as IteratorClose does.
8537pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8538    let has = crate::builtins::get_property(iterator, "return")?;
8539    if with_host(|h| is_callable(h, &has)) {
8540        call_method(iterator, "return", Vec::new())?;
8541    }
8542    Ok(())
8543}
8544
8545pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8546    let mut out = Vec::new();
8547    loop {
8548        let step = call_method(iterator, "next", Vec::new())?;
8549        let done = get_prop_chain(&step, "done")?;
8550        if with_host(|h| h.truthy(&done)) {
8551            break;
8552        }
8553        out.push(get_prop_chain(&step, "value")?);
8554    }
8555    Ok(out)
8556}
8557
8558/// Property read that walks the prototype chain (used by iteration helpers).
8559/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8560/// `Math.floor`); the exceptions are the namespace objects a script can only
8561/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8562/// report `typeof === "object"` and carry no `name`/`length`.
8563pub fn builtin_is_callable(n: &str) -> bool {
8564    // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8565    // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8566    // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8567    // `true` to `instanceof Function`. `Function.prototype` is the one that
8568    // really IS callable (10.2.4: it is an anonymous built-in that returns
8569    // undefined), which is why it is not stripped here.
8570    if n != "Function.prototype" && n.ends_with(".prototype") {
8571        return false;
8572    }
8573    // A `match` rather than a slice scan: this runs on every callability test,
8574    // which is every call and every `ToPrimitive`, and a `contains` over the
8575    // list below compares against all 56 entries before answering "callable" —
8576    // the common case. The compiler turns the arms into a length-then-bytes
8577    // decision tree instead.
8578    !matches!(
8579        n,
8580        "Math"
8581            | "JSON"
8582            | "console"
8583            | "Reflect"
8584            | "process"
8585            | "Atomics"
8586            | "performance"
8587            | "fs"
8588            | "path"
8589            | "os"
8590            | "util"
8591            | "crypto"
8592            | "webcrypto"
8593            | "SubtleCrypto"
8594            | "querystring"
8595            | "events"
8596            | "timers"
8597            | "perf_hooks"
8598            | "async_hooks"
8599            | "diagnostics_channel"
8600            | "v8"
8601            | "dns"
8602            | "punycode"
8603            | "child_process"
8604            | "tty"
8605            | "url"
8606            | "zlib"
8607            | "string_decoder"
8608            | "http"
8609            | "net"
8610            | "buffer"
8611            | "function"
8612            | "path/win32"
8613            | "fs/promises"
8614            | "stream/promises"
8615            | "stream/consumers"
8616            | "stream/web"
8617            | "timers/promises"
8618            | "dns/promises"
8619            | "https"
8620            | "http2"
8621            | "tls"
8622            | "dgram"
8623            | "cluster"
8624            | "worker_threads"
8625            | "readline"
8626            | "readline/promises"
8627            | "repl"
8628            | "vm"
8629            | "domain"
8630            | "trace_events"
8631            | "wasi"
8632            | "inspector"
8633            | "object"
8634    )
8635        // The live `require.cache` view is a plain object to a script, not
8636        // something it can call.
8637        && n != crate::builtins::REQUIRE_CACHE
8638}
8639
8640pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8641    crate::builtins::get_property(recv, name)
8642}
8643
8644/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8645/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8646/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8647/// native-tagged instances) is an object and must be converted.
8648pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8649    match v {
8650        Value::Obj(_) => matches!(
8651            h.get(v),
8652            None | Some(JsObj::Null)
8653                | Some(JsObj::Str(_))
8654                | Some(JsObj::Symbol { .. })
8655                | Some(JsObj::BigInt(_))
8656        ),
8657        _ => true,
8658    }
8659}
8660
8661/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8662/// or `"string"`.
8663///
8664/// An object carrying a `Symbol.toPrimitive` method (internal key
8665/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8666/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8667/// the order reversed for the string hint — and takes the FIRST call whose
8668/// result is a primitive. An object that yields no primitive (a null-prototype
8669/// object has neither method) throws V8's
8670/// `TypeError: Cannot convert object to primitive value`.
8671///
8672/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8673/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8674/// to read `str_of` directly and so never invoked a user `valueOf`.
8675pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8676    if with_host(|h| is_primitive(h, v)) {
8677        return Ok(v.clone());
8678    }
8679    if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8680        if with_host(|h| is_callable(h, &f)) {
8681            let hv = with_host(|h| h.new_str(hint.to_string()));
8682            let r = invoke(&f, vec![hv], Some(v.clone()))?;
8683            if with_host(|h| is_primitive(h, &r)) {
8684                return Ok(r);
8685            }
8686            return Err(type_error("Cannot convert object to primitive value"));
8687        }
8688    }
8689    // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8690    // `"string"`, which is why `new Date() + 1` concatenates while
8691    // `new Date() - 1` is arithmetic.
8692    let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8693        "string"
8694    } else {
8695        hint
8696    };
8697    let order = if hint == "string" {
8698        ["toString", "valueOf"]
8699    } else {
8700        ["valueOf", "toString"]
8701    };
8702    // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8703    // below is for exotics whose property funnel exposes no callable
8704    // `toString`, not for an object whose own methods ran and returned
8705    // non-primitives — that case is the spec's TypeError, and branding it
8706    // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8707    // quietly produced `"[object Object]1"`.
8708    let mut called_any = false;
8709    for m in order {
8710        let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8711        if !with_host(|h| is_callable(h, &f)) {
8712            continue;
8713        }
8714        called_any = true;
8715        // On a Proxy the resolved method is a thunk bound to the TARGET, so
8716        // invoking it directly would stringify the target — `String(new
8717        // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8718        // native-code form. `call_method` re-dispatches the generic
8719        // `Function.prototype`/`Object.prototype` methods against the proxy.
8720        let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8721            call_method(v, m, Vec::new())?
8722        } else {
8723            invoke(&f, Vec::new(), Some(v.clone()))?
8724        };
8725        if with_host(|h| is_primitive(h, &r)) {
8726            return Ok(r);
8727        }
8728    }
8729    // Every object except a null-prototype one inherits `Object.prototype
8730    // .toString`, which always returns a string — so the exhausted-methods
8731    // TypeError is reachable only there. The exotics whose property funnel has
8732    // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8733    // get the same `[object Tag]` brand V8 gives them.
8734    // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8735    // for both method names, and if what came back was not callable there is
8736    // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8737    // is a TypeError on node, where branding it `[object Object]` invented a
8738    // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8739    // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8740    // the target's `[object Map]` brand exactly as the bare `Map` does.
8741    if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8742        return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8743    }
8744    Err(type_error("Cannot convert object to primitive value"))
8745}
8746
8747/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8748/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8749/// is absent or returns an object — is invoked), then rendered by `str_of`.
8750/// Returns a heap string value.
8751pub fn to_string_value(v: &Value) -> Result<Value, String> {
8752    let p = to_primitive(v, "string")?;
8753    // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8754    // refuses. `String(sym)` is the documented exception and is handled at that
8755    // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8756    // `[sym].join()`) rejects the way node does instead of silently rendering
8757    // `Symbol(desc)`.
8758    if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8759        return Err(type_error("Cannot convert a Symbol value to a string"));
8760    }
8761    Ok(with_host(|h| {
8762        let s = h.str_of(&p);
8763        h.new_str(s)
8764    }))
8765}
8766
8767/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8768/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8769pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8770    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8771        return Ok(with_host(|h| {
8772            let s = h.str_of(v);
8773            h.new_str(s)
8774        }));
8775    }
8776    to_string_value(v)
8777}
8778
8779/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8780/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8781/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8782/// under the host borrow and so can never invoke a JS `valueOf`.
8783pub fn to_number_value(v: &Value) -> Result<f64, String> {
8784    // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8785    // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8786    // and `+Symbol()` are both `TypeError` on node v26.7.0.
8787    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8788        return Err(type_error("Cannot convert a Symbol value to a number"));
8789    }
8790    if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8791        return Ok(n);
8792    }
8793    let p = to_primitive(v, "number")?;
8794    // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8795    // the argument was checked, so an object whose conversion yields a symbol or
8796    // a BigInt slipped past: `+Object(9n)` answered 9 and
8797    // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8798    // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8799    // which is why `Number(Object(9n))` is still 9.
8800    match with_host(|h| h.get(&p).cloned()) {
8801        Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8802        Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8803        _ => Ok(with_host(|h| h.to_number(&p))),
8804    }
8805}
8806
8807/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8808/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8809/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8810pub fn to_property_key(v: &Value) -> Result<String, String> {
8811    // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8812    // `o[sym]`); only an object key pays for the conversion.
8813    if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8814        return Ok(k);
8815    }
8816    let p = to_primitive(v, "string")?;
8817    Ok(with_host(|h| h.str_of(&p)))
8818}
8819
8820/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8821/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8822/// target), so `typeof` and every `is_callable` guard agree on one answer.
8823pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8824    match h.get(v) {
8825        // Not every builtin is a function: the namespace objects (`Math`,
8826        // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8827        // calling one is a `TypeError` in node exactly as `typeof` says.
8828        Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8829        Some(JsObj::Func(_))
8830        | Some(JsObj::BoundMethod { .. })
8831        | Some(JsObj::BoundFunc { .. })
8832        | Some(JsObj::Class(_)) => true,
8833        Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8834        _ => false,
8835    }
8836}
8837
8838/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8839/// stored value (methods, inherited data props). Does NOT invoke accessors.
8840/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8841/// `then` and the rest — which the spec performs with `[[Get]]`.
8842///
8843/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8844/// map and never asks the handler, so a proxy supplying a protocol method
8845/// through its `get` trap was invisible and the operation fell back to the
8846/// default. Everything else takes the cheap chain walk.
8847pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8848    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8849        let got = crate::builtins::get_property(v, key)?;
8850        return Ok((!matches!(got, Value::Undef)).then_some(got));
8851    }
8852    Ok(with_host(|h| lookup_chain(h, v, key)))
8853}
8854
8855pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8856    if let Some(JsObj::Object(p)) = h.get(recv) {
8857        if let Some(v) = p.get(key) {
8858            return Some(v.clone());
8859        }
8860    }
8861    let mut cur = h.proto_of(recv);
8862    while let Some(p) = cur {
8863        // A chain link may be a plain object OR a function/class (the `router`
8864        // package sets `Router.prototype = function(){}` and hangs its methods off
8865        // that function, so the methods live in the fn-prop side table).
8866        match h.get(&p) {
8867            Some(JsObj::Object(props)) => {
8868                if let Some(v) = props.get(key) {
8869                    return Some(v.clone());
8870                }
8871            }
8872            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8873                if let Some(v) = h.fn_prop(&p, key) {
8874                    return Some(v);
8875                }
8876            }
8877            _ => {}
8878        }
8879        cur = h.proto_of(&p);
8880    }
8881    None
8882}
8883
8884/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8885pub fn lookup_accessor(
8886    h: &JsHost,
8887    recv: &Value,
8888    key: &str,
8889) -> Option<(Option<Value>, Option<Value>)> {
8890    if let Some(a) = h.own_accessor(recv, key) {
8891        return Some(a);
8892    }
8893    let mut cur = h.proto_of(recv);
8894    while let Some(p) = cur {
8895        if let Some(a) = h.own_accessor(&p, key) {
8896            return Some(a);
8897        }
8898        cur = h.proto_of(&p);
8899    }
8900    // A STATIC accessor declared by an ancestor class. A subclass reaches its
8901    // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8902    // above reads — classes are not linked there, so that walk ended at once.
8903    // Static methods and fields already inherited because `class_static` does
8904    // this same parent walk for `fn_prop`; only accessors had no equivalent:
8905    //
8906    //     class Base { static get kind() { return 'base' } }
8907    //     class Sub extends Base {}
8908    //     Sub.plain()  // worked, a fn_prop
8909    //     Sub.kind     // undefined; node reads 'base'
8910    //
8911    // The caller invokes the getter with the class it was READ off as `this`,
8912    // so a getter reading `this.x` sees the subclass, per 10.2.4.
8913    let mut cls = recv.clone();
8914    while let Some(JsObj::Class(c)) = h.get(&cls) {
8915        let Some(parent) = c.parent.clone() else {
8916            break;
8917        };
8918        if let Some(a) = h.own_accessor(&parent, key) {
8919            return Some(a);
8920        }
8921        cls = parent;
8922    }
8923    None
8924}
8925
8926/// Register a builtin error prototype (for `instanceof Error` etc.).
8927pub fn set_error_proto(name: &str, proto: Value) {
8928    with_host(|h| {
8929        h.error_protos.insert(name.to_string(), proto);
8930    });
8931}
8932pub fn error_proto(name: &str) -> Option<Value> {
8933    with_host(|h| h.error_protos.get(name).cloned())
8934}
8935/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8936pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8937    h.error_protos.get(name).cloned()
8938}
8939
8940impl JsHost {
8941    /// `Error.prototype.toString` for an object whose prototype chain reaches
8942    /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8943    /// `None` for anything that is not an error, so the caller keeps its own
8944    /// stringification.
8945    pub fn error_to_string(&self, v: &Value) -> Option<String> {
8946        let base = self.error_protos.get("Error")?;
8947        let mut cur = self.proto_of(v);
8948        let mut is_error = false;
8949        while let Some(p) = cur {
8950            if self.strict_eq(&p, base) {
8951                is_error = true;
8952                break;
8953            }
8954            cur = self.proto_of(&p);
8955        }
8956        if !is_error {
8957            return None;
8958        }
8959        let name = lookup_chain(self, v, "name")
8960            .map(|n| self.str_of(&n))
8961            .unwrap_or_else(|| "Error".into());
8962        let message = lookup_chain(self, v, "message")
8963            .map(|m| self.str_of(&m))
8964            .unwrap_or_default();
8965        // Node's internal coded errors override `toString` as
8966        // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8967        // `@@nodeError` tag marks the errors `synth_error` built from a
8968        // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8969        // property still stringifies plainly.
8970        if let Some(JsObj::Object(p)) = self.get(v) {
8971            if p.contains_key("@@nodeError") {
8972                if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8973                    return Some(format!("{name} [{code}]: {message}"));
8974                }
8975            }
8976        }
8977        Some(match (name.is_empty(), message.is_empty()) {
8978            (true, _) => message,
8979            (false, true) => name,
8980            (false, false) => format!("{name}: {message}"),
8981        })
8982    }
8983}
8984
8985/// The set of builtin error constructor names forming the error hierarchy.
8986pub const ERROR_NAMES: &[&str] = &[
8987    "Error",
8988    "TypeError",
8989    "RangeError",
8990    "SyntaxError",
8991    "ReferenceError",
8992    "EvalError",
8993    "URIError",
8994    "AggregateError",
8995    // `assert`'s error class. It is NOT a global (node exposes it only as
8996    // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8997    // has to be a name `synth_error` recognizes: without it the head
8998    // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8999    // the `Error` branch with the WHOLE head kept as the message, so `e.name`
9000    // was `Error` and `e.message` carried a prefix node keeps out of it.
9001    "AssertionError",
9002    // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
9003    // `name` comes from the SECOND constructor argument rather than from the
9004    // class, so its prototype keeps the base default and each instance stamps
9005    // its own name into an internal slot.
9006    "DOMException",
9007];
9008
9009impl JsHost {
9010    /// Lazily build the builtin error prototype chain: `Error.prototype →
9011    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
9012    /// Populated once; instances link to these so `e instanceof TypeError` and
9013    /// `e instanceof Error` both hold.
9014    /// The real `Buffer.prototype` object, building the
9015    /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
9016    /// first use.
9017    ///
9018    /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
9019    /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
9020    /// `instanceof` had to be special-cased around it. Each prototype is a
9021    /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
9022    /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
9023    /// way it did when `Buffer.prototype` was a `Builtin` namespace.
9024    pub fn ensure_native_protos(&mut self) {
9025        // The wrapper prototypes share this registry and this guard would skip
9026        // them, so they are built through their own.
9027        self.ensure_wrapper_protos();
9028        self.ensure_function_kind_protos();
9029        if self.native_protos.contains_key("Buffer") {
9030            return;
9031        }
9032        let obj_proto = self.object_proto();
9033        // `Object.prototype` is the one builtin prototype that already existed as
9034        // a real object (it is the chain root). Register it so `Object.prototype`
9035        // reads resolve to THAT object rather than a fresh `Builtin` namespace —
9036        // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
9037        // compares a real object against a thunk and reads false.
9038        self.native_protos
9039            .insert("Object".to_string(), obj_proto.clone());
9040        for m in crate::builtins::OBJECT_PROTO_METHODS {
9041            let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
9042            if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
9043                p.insert((*m).to_string(), thunk);
9044            }
9045            self.hide_prop(&obj_proto, m);
9046        }
9047        // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
9048        // Object.prototype`, which is the chain node v26.7.0 really has. The
9049        // shared iteration methods (`every`, `map`, `filter`, …) live on the
9050        // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
9051        // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
9052        // Node while the intermediate owns it. `%TypedArray%` is not a global,
9053        // so it is reachable only by walking the chain — exactly as in Node.
9054        // Every element kind gets its own prototype hanging off the shared
9055        // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
9056        // `Int32Array.prototype` rather than some other kind's. Linking them all
9057        // to `Uint8Array.prototype` would have been the easy version and would
9058        // have made an `Int32Array` claim the wrong prototype.
9059        let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
9060        for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
9061            chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
9062        }
9063        // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
9064        chain.push(("Buffer", Value::Undef));
9065        let mut prev: Option<Value> = None;
9066        for (ctor, parent) in chain.drain(..) {
9067            let proto = self.new_object(IndexMap::new());
9068            // Each kind hangs off the shared intermediate; `Buffer` hangs off
9069            // `Uint8Array.prototype`; the intermediate itself off
9070            // `Object.prototype`.
9071            let parent = match ctor {
9072                "TypedArray" => parent,
9073                "Buffer" => self
9074                    .native_protos
9075                    .get("Uint8Array")
9076                    .cloned()
9077                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
9078                _ => self
9079                    .native_protos
9080                    .get("TypedArray")
9081                    .cloned()
9082                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
9083            };
9084            self.set_proto(&proto, parent);
9085            // `%TypedArray%.prototype` has no reachable constructor global, so
9086            // it gets no `constructor` slot (Node's is the anonymous
9087            // `%TypedArray%` intrinsic).
9088            if ctor != "TypedArray" {
9089                let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9090                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9091                    p.insert("constructor".into(), ctor_val);
9092                }
9093                self.hide_prop(&proto, "constructor");
9094            }
9095            let methods: &[&str] = match ctor {
9096                "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
9097                "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
9098                // `Uint8Array` alone owns the base64/hex pair — no other view
9099                // has them, which is the whole reason they cannot live on the
9100                // shared `%TypedArray%` prototype above.
9101                "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
9102                // Every other kind's prototype owns no methods; it inherits them
9103                // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
9104                // which is per-kind and which Node really keeps there (measured:
9105                // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
9106                _ => &[],
9107            };
9108            if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
9109                let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
9110                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9111                    p.insert("BYTES_PER_ELEMENT".into(), bpe);
9112                }
9113                self.hide_prop(&proto, "BYTES_PER_ELEMENT");
9114            }
9115            for m in methods {
9116                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9117                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9118                    p.insert((*m).to_string(), thunk);
9119                }
9120                self.hide_prop(&proto, m);
9121            }
9122            self.native_protos.insert(ctor.to_string(), proto.clone());
9123            prev = Some(proto);
9124        }
9125    }
9126
9127    /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
9128    /// objects.
9129    ///
9130    /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
9131    /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
9132    /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
9133    /// String.prototype` and `w instanceof String` both read false while the
9134    /// wrapper's methods still resolved through the string funnel. Registering
9135    /// them here puts them on the same footing as `Buffer.prototype`.
9136    /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
9137    /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
9138    /// function's `[[Prototype]]` really points at.
9139    ///
9140    /// None are globals (node exposes them only through
9141    /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
9142    /// rather than among the wrapper constructors. Each hangs off
9143    /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
9144    pub fn ensure_function_kind_protos(&mut self) {
9145        if self.native_protos.contains_key("GeneratorFunction") {
9146            return;
9147        }
9148        let base = self
9149            .native_protos
9150            .get("Function")
9151            .cloned()
9152            .unwrap_or_else(|| self.object_proto());
9153        for ctor in [
9154            "GeneratorFunction",
9155            "AsyncFunction",
9156            "AsyncGeneratorFunction",
9157        ] {
9158            let proto = self.new_object(IndexMap::new());
9159            self.set_proto(&proto, base.clone());
9160            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9161            let tag = self.new_str(ctor);
9162            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9163                p.insert("constructor".into(), ctor_val);
9164                p.insert("@@toStringTag".into(), tag);
9165            }
9166            self.hide_prop(&proto, "constructor");
9167            self.hide_prop(&proto, "@@toStringTag");
9168            self.native_protos.insert(ctor.to_string(), proto);
9169        }
9170    }
9171
9172    pub fn ensure_wrapper_protos(&mut self) {
9173        if self.native_protos.contains_key("String") {
9174            return;
9175        }
9176        let obj_proto = self.object_proto();
9177        for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9178            let proto = self.new_object(IndexMap::new());
9179            self.set_proto(&proto, obj_proto.clone());
9180            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9181            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9182                p.insert("constructor".into(), ctor_val);
9183            }
9184            self.hide_prop(&proto, "constructor");
9185            // The three conversions must be here because `Object.prototype`
9186            // also defines them: without a shadowing entry a wrapper would
9187            // inherit the object forms and `String(new String("a"))` would
9188            // report `[object Object]`.
9189            //
9190            // The REST are here because the prototype is a real object a script
9191            // can read a method OFF of. Only the three were installed, on the
9192            // reasoning that `charAt`/`toFixed`/… reach the primitive through
9193            // `call_method` anyway — true for `s.charAt(0)` and false for the
9194            // generic-borrowing form: `String.prototype.trim` read `undefined`,
9195            // so `String.prototype.trim.call(s)` — and `Number.prototype
9196            // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9197            // wrapper method — threw. `Array.prototype`/`Object.prototype`
9198            // already carried their whole method set; these three did not.
9199            let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9200                .into_iter()
9201                .chain(match ctor {
9202                    "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9203                    "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9204                    _ => [].iter().copied(),
9205                })
9206                // The symbol-keyed methods come from the generated intrinsic
9207                // table, so this object advertises exactly the symbol methods
9208                // node defines on it — `String.prototype[Symbol.iterator]` was
9209                // `undefined` because only the string-keyed lists were walked.
9210                .chain(crate::builtins::proto_symbol_methods(ctor))
9211                .collect();
9212            let mut seen: Vec<&str> = Vec::new();
9213            for m in methods {
9214                if seen.contains(&m) {
9215                    continue;
9216                }
9217                seen.push(m);
9218                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9219                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9220                    p.insert(m.to_string(), thunk);
9221                }
9222                self.hide_prop(&proto, m);
9223            }
9224            // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9225            // prototypes that carry a `@@toStringTag`; the other three are
9226            // branded by their internal slot instead, and node reports
9227            // `undefined` for their tag. Without it
9228            // `Object.prototype.toString.call(Symbol.prototype)` read
9229            // `[object Object]` where node says `[object Symbol]`.
9230            if matches!(ctor, "Symbol" | "BigInt") {
9231                let tag = self.new_str(ctor.to_string());
9232                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9233                    p.insert("@@toStringTag".into(), tag);
9234                }
9235                // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9236                // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9237                // writable.
9238                self.set_prop_attrs(
9239                    &proto,
9240                    "@@toStringTag",
9241                    PropAttrs {
9242                        writable: false,
9243                        enumerable: false,
9244                        configurable: true,
9245                    },
9246                );
9247            }
9248            // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9249            // numeric conversion of a symbol reaches FIRST. Its absence was
9250            // observable in the failure wording: `String(Symbol.prototype)`
9251            // throws in node because `@@toPrimitive` rejects a non-Symbol
9252            // `this`, and here the conversion fell through to `toString` and
9253            // named that method in the message instead.
9254            if ctor == "Symbol" {
9255                let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9256                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9257                    p.insert("@@toPrimitive".into(), thunk);
9258                }
9259                self.hide_prop(&proto, "@@toPrimitive");
9260            }
9261            self.native_protos.insert(ctor.to_string(), proto);
9262        }
9263    }
9264
9265    /// The cached template object for one tagged-template site, if it has been
9266    /// evaluated before.
9267    pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9268        self.template_objects.get(&key).cloned()
9269    }
9270    /// Record the template object for one tagged-template site.
9271    pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9272        self.template_objects.insert(key, v);
9273    }
9274
9275    /// The real prototype object for a builtin exotic, if it has one.
9276    pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9277        self.native_protos.get(ctor).cloned()
9278    }
9279
9280    /// The constructor name whose `.prototype` object IS `v`, for a prototype
9281    /// this host built as a real object (`String.prototype`, `TypeError
9282    /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9283    ///
9284    /// A prototype is an ORDINARY object: it carries no instance's internal
9285    /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9286    /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9287    /// before, so the brand fell through to the "does it look like an Error"
9288    /// test and answered for the prototype as if it were an instance.
9289    pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9290        if !matches!(v, Value::Obj(_)) {
9291            return None;
9292        }
9293        self.native_protos
9294            .iter()
9295            .chain(self.error_protos.iter())
9296            .find(|(_, p)| *p == v)
9297            .map(|(name, _)| name.as_str())
9298    }
9299
9300    /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9301    /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9302    ///
9303    /// `Ctor.prototype` used to read `undefined` for every native class outside the
9304    /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9305    /// that libraries still use. `iconv-lite`'s internal codec — reached from
9306    /// `raw-body` on every `express.json()` request — does exactly this:
9307    ///
9308    /// ```text
9309    /// var StringDecoder = require('string_decoder').StringDecoder;
9310    /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9311    /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9312    /// InternalDecoder.prototype = StringDecoder.prototype;
9313    /// ```
9314    ///
9315    /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9316    ///
9317    /// Methods come from `stdlib::instance_method_lists`, the same table a method
9318    /// READ consults, so the prototype can never advertise a name the dispatcher
9319    /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9320    /// dispatches against its invoke-time `this`, so a subclass instance whose
9321    /// prototype IS this object gets the native implementation. Returns `None` for
9322    /// a tag with no instance methods, leaving those constructors as they were.
9323    pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9324        if let Some(p) = self.native_protos.get(ctor) {
9325            return Some(p.clone());
9326        }
9327        // `Buffer` and the typed-array kinds belong to the chain
9328        // `ensure_native_protos` builds. Building one of them here first hung it
9329        // straight off `Object.prototype` AND registered it, which made that
9330        // chain's own `contains_key("Buffer")` guard skip the build for the rest
9331        // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9332        // Uint8Array` read false.
9333        if ctor == "Buffer"
9334            || ctor == "TypedArray"
9335            || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9336        {
9337            self.ensure_native_protos();
9338            return self.native_protos.get(ctor).cloned();
9339        }
9340        let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9341        // A class can carry accessors and no methods at all
9342        // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9343        // `asymmetricKeyDetails`), so an empty method list does not mean there
9344        // is no prototype to build.
9345        let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9346        if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9347            return None;
9348        }
9349        // A native class with a real PARENT hangs off that parent's prototype
9350        // rather than straight off `Object.prototype`. The stream hierarchy is
9351        // `Readable → Stream → EventEmitter`, which is what makes
9352        // `new Readable() instanceof Stream` hold and what an ES5 subclass
9353        // doing `Object.create(Stream.prototype)` inherits from.
9354        let parent_proto = match crate::stdlib::native_parent(ctor) {
9355            Some(p) => self
9356                .ensure_ctor_proto(p)
9357                .unwrap_or_else(|| self.object_proto()),
9358            None => self.object_proto(),
9359        };
9360        let proto = self.new_object(IndexMap::new());
9361        self.set_proto(&proto, parent_proto);
9362        let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9363        if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9364            p.insert("constructor".into(), ctor_val);
9365        }
9366        self.hide_prop(&proto, "constructor");
9367        // A prototype member is ENUMERABLE in node for every class but the few
9368        // written as ES classes, so `for (const k in url)` walks `href` and the
9369        // rest. Hiding all of them made that loop find nothing.
9370        let visible = crate::stdlib::instance_members_enumerable(ctor);
9371        let symbols = crate::builtins::proto_symbol_methods(ctor);
9372        for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9373            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9374            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9375                p.insert((*m).to_string(), thunk);
9376            }
9377            // A symbol-keyed member never enumerates.
9378            if !visible || m.starts_with("@@") {
9379                self.hide_prop(&proto, m);
9380            }
9381        }
9382        // Accessors and the class's `Symbol.toStringTag`, both of which live on
9383        // the PROTOTYPE in node — an instance owns neither.
9384        let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9385        for (key, settable) in accessors {
9386            let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9387            let set =
9388                settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9389            self.set_accessor(&proto, key, Some(get), set);
9390            if !visible {
9391                self.hide_prop(&proto, key);
9392            }
9393        }
9394        for m in crate::stdlib::instance_late_methods(ctor) {
9395            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9396            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9397                p.insert((*m).to_string(), thunk);
9398            }
9399            if !visible {
9400                self.hide_prop(&proto, m);
9401            }
9402        }
9403        if !tag.is_empty() {
9404            let tag = self.new_str(tag.to_string());
9405            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9406                p.insert("@@toStringTag".into(), tag);
9407            }
9408            self.hide_prop(&proto, "@@toStringTag");
9409        }
9410        self.native_protos.insert(ctor.to_string(), proto.clone());
9411        Some(proto)
9412    }
9413
9414    /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9415    /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9416    /// which answers from `native_protos` — does not find them.
9417    pub fn error_proto(&self, name: &str) -> Option<Value> {
9418        self.error_protos.get(name).cloned()
9419    }
9420
9421    pub fn ensure_error_protos(&mut self) {
9422        if !self.error_protos.is_empty() {
9423            return;
9424        }
9425        let obj_proto = self.object_proto();
9426        // Error.prototype first (the shared base).
9427        let err_proto = self.new_object(IndexMap::new());
9428        self.set_proto(&err_proto, obj_proto);
9429        let nm = self.new_str("Error");
9430        let empty = self.new_str("");
9431        let ctor = self.alloc(JsObj::Builtin("Error".into()));
9432        // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9433        // not a fallback the stringifier applies when nothing else matches: it
9434        // exists precisely to shadow `Object.prototype.toString`. Without it,
9435        // the first read of `Error.prototype` or `Object.prototype` — which
9436        // `x instanceof Error` performs, so ordinary code triggers it —
9437        // materialised `Object.prototype.toString`, the chain lookup started
9438        // finding it, and `String(err)` flipped from `Error: m` to
9439        // `[object Error]` for the REST OF THE PROCESS, including errors
9440        // created before the read.
9441        let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9442        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9443            p.insert("name".into(), nm);
9444            p.insert("message".into(), empty);
9445            p.insert("constructor".into(), ctor);
9446            p.insert("toString".into(), to_string);
9447        }
9448        // Everything on `Error.prototype` is non-enumerable in V8.
9449        for k in ["name", "message", "constructor", "toString"] {
9450            self.hide_prop(&err_proto, k);
9451        }
9452        self.error_protos.insert("Error".into(), err_proto.clone());
9453        for name in &ERROR_NAMES[1..] {
9454            let p = self.new_object(IndexMap::new());
9455            self.set_proto(&p, err_proto.clone());
9456            let nm = self.new_str(*name);
9457            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9458            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9459                o.insert("name".into(), nm);
9460                o.insert("constructor".into(), ctor);
9461            }
9462            self.hide_prop(&p, "name");
9463            self.hide_prop(&p, "constructor");
9464            self.error_protos.insert((*name).to_string(), p);
9465        }
9466    }
9467}
9468
9469// ── Map/Set element access (used by builtins) ────────────────────────────────
9470
9471impl JsHost {
9472    /// A function's `.length`: the count of leading params before the first one
9473    /// with a default or the rest element.
9474    pub fn func_arity(&self, v: &Value) -> usize {
9475        // 20.2.3.2: a bound function's `length` is the target's, less the
9476        // arguments already bound, floored at 0. Reporting 0 for every bound
9477        // function breaks arity dispatch — express picks error-handling
9478        // middleware with `fn.length === 4`, so a bound handler was never
9479        // recognised as one.
9480        if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9481            return self.func_arity(&target.clone()).saturating_sub(args.len());
9482        }
9483        // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9484        // reports 1 rather than the 0 a target of unknown arity would give.
9485        if let Some(JsObj::Builtin(n)) = self.get(v) {
9486            return crate::builtins::builtin_meta(n)
9487                .map(|(_, len)| len as usize)
9488                .unwrap_or(0);
9489        }
9490        let def_id = match self.get(v) {
9491            Some(JsObj::Func(f)) => Some(f.def_id),
9492            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9493                Some(JsObj::Func(f)) => Some(f.def_id),
9494                _ => None,
9495            },
9496            _ => None,
9497        };
9498        match def_id.and_then(|id| self.funcs.get(id)) {
9499            Some(def) => def
9500                .params
9501                .iter()
9502                .take_while(|p| !p.rest && !p.has_default)
9503                .count(),
9504            None => 0,
9505        }
9506    }
9507
9508    pub fn is_map(&self, v: &Value) -> bool {
9509        matches!(self.get(v), Some(JsObj::Map { .. }))
9510    }
9511    pub fn is_set(&self, v: &Value) -> bool {
9512        matches!(self.get(v), Some(JsObj::Set { .. }))
9513    }
9514}
9515
9516// ── promises & the event loop ────────────────────────────────────────────────
9517
9518impl JsHost {
9519    /// Allocate a fresh pending promise, returning its heap value.
9520    pub fn new_promise(&mut self) -> Value {
9521        let id = self.promises.len() as u32;
9522        self.promises.push(PromiseCell {
9523            state: PromiseState::Pending,
9524            value: Value::Undef,
9525            reactions: Vec::new(),
9526            handled: false,
9527        });
9528        self.alloc(JsObj::Promise { id })
9529    }
9530    pub fn promise_id(&self, v: &Value) -> Option<u32> {
9531        match self.get(v) {
9532            Some(JsObj::Promise { id }) => Some(*id),
9533            _ => None,
9534        }
9535    }
9536    pub fn promise_state(&self, id: u32) -> PromiseState {
9537        self.promises[id as usize].state
9538    }
9539    pub fn promise_value(&self, id: u32) -> Value {
9540        self.promises[id as usize].value.clone()
9541    }
9542    pub fn promise_mark_handled(&mut self, id: u32) {
9543        self.promises[id as usize].handled = true;
9544    }
9545    /// Take the pending reactions of a promise (called on settle).
9546    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9547        std::mem::take(&mut self.promises[id as usize].reactions)
9548    }
9549    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9550        self.promises[id as usize].reactions.push(r);
9551    }
9552    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9553        let c = &mut self.promises[id as usize];
9554        if c.state != PromiseState::Pending {
9555            return; // already settled — resolve/reject are one-shot
9556        }
9557        c.state = state;
9558        c.value = value;
9559    }
9560    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9561        self.microtasks.push_back(Task::Js { cb, args });
9562    }
9563    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9564        self.nextticks.push_back(Task::Js { cb, args });
9565    }
9566    /// Schedule a native (Rust) microtask — used by Promise reactions and async
9567    /// resumption.
9568    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9569        self.microtasks.push_back(Task::Native(f));
9570    }
9571    /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9572    /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9573    /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9574    /// `ref`/`unref` and `refresh` can find this entry again.
9575    pub fn add_timer(
9576        &mut self,
9577        delay: f64,
9578        callback: Value,
9579        args: Vec<Value>,
9580        interval: Option<f64>,
9581    ) -> u64 {
9582        let id = self.next_timer;
9583        self.next_timer += 1;
9584        // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9585        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9586        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9587        self.macrotasks.push(Timer {
9588            id,
9589            delay,
9590            seq: id,
9591            callback,
9592            args,
9593            cancelled: false,
9594            interval,
9595            refed: true,
9596            deadline,
9597        });
9598        id
9599    }
9600    /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9601    /// `clearInterval` from *inside* the callback cancels this very entry) and
9602    /// taking a fresh `seq` so same-delay peers still round-robin.
9603    ///
9604    /// Called BEFORE the callback runs: if it were called after, the entry would
9605    /// be absent while the callback executed and a `clearInterval(t)` there would
9606    /// cancel nothing, resurrecting an interval the program had stopped.
9607    fn rearm_timer(&mut self, t: &Timer, period: f64) {
9608        let seq = self.next_timer;
9609        self.next_timer += 1;
9610        let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9611        self.macrotasks.push(Timer {
9612            id: t.id,
9613            delay: t.delay,
9614            seq,
9615            callback: t.callback.clone(),
9616            args: t.args.clone(),
9617            cancelled: false,
9618            interval: Some(period),
9619            refed: t.refed,
9620            deadline,
9621        });
9622    }
9623    /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9624    /// timer. A no-op once the timer has fired or been cleared (Node likewise
9625    /// treats `ref`/`unref` on a dead timer as inert).
9626    pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9627        for t in &mut self.macrotasks {
9628            if t.id == id && !t.cancelled {
9629                t.refed = refed;
9630            }
9631        }
9632    }
9633    /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9634    /// A fired or cleared timer reports `false`, matching Node.
9635    pub fn timer_has_ref(&self, id: u64) -> bool {
9636        self.macrotasks
9637            .iter()
9638            .any(|t| t.id == id && !t.cancelled && t.refed)
9639    }
9640    /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9641    /// just been scheduled.
9642    pub fn refresh_timer(&mut self, id: u64) {
9643        let now = Instant::now();
9644        for t in &mut self.macrotasks {
9645            if t.id == id && !t.cancelled {
9646                t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9647            }
9648        }
9649    }
9650    /// Clone the I/O sender for a background I/O thread.
9651    pub fn io_sender(&self) -> Sender<IoTask> {
9652        self.io_tx.clone()
9653    }
9654    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9655    /// alive.
9656    pub fn incr_handle(&mut self) {
9657        self.open_handles += 1;
9658    }
9659    /// Release a handle; the loop exits once this reaches `0` with empty queues.
9660    pub fn decr_handle(&mut self) {
9661        self.open_handles = self.open_handles.saturating_sub(1);
9662    }
9663    pub fn open_handles(&self) -> usize {
9664        self.open_handles
9665    }
9666    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9667    /// path). Ties break by `seq`.
9668    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9669        let idx = self
9670            .macrotasks
9671            .iter()
9672            .enumerate()
9673            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9674            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9675            .map(|(i, _)| i);
9676        idx.map(|i| self.macrotasks.remove(i))
9677    }
9678    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9679    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9680    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9681        self.macrotasks
9682            .iter()
9683            .filter(|t| !t.cancelled)
9684            .map(|t| t.deadline)
9685            .min()
9686            .map(|d| d.saturating_duration_since(now))
9687    }
9688    pub fn cancel_timer(&mut self, id: u64) {
9689        for t in &mut self.macrotasks {
9690            if t.id == id {
9691                t.cancelled = true;
9692            }
9693        }
9694    }
9695    fn pop_next_timer(&mut self) -> Option<Timer> {
9696        // Earliest (delay, seq) fires first — a deterministic virtual clock.
9697        let idx = self
9698            .macrotasks
9699            .iter()
9700            .enumerate()
9701            .filter(|(_, t)| !t.cancelled)
9702            .min_by(|(_, a), (_, b)| {
9703                a.delay
9704                    .partial_cmp(&b.delay)
9705                    .unwrap_or(std::cmp::Ordering::Equal)
9706                    .then(a.seq.cmp(&b.seq))
9707            })
9708            .map(|(i, _)| i);
9709        idx.map(|i| self.macrotasks.remove(i))
9710    }
9711    fn next_microtask(&mut self) -> Option<Task> {
9712        // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9713        // queue, then drain the microtask queue in full, then repeat if the
9714        // microtasks queued more ticks. A tick queued from INSIDE a microtask
9715        // therefore waits for the rest of that microtask queue.
9716        //
9717        // Preferring ticks on every step interleaved the two, so
9718        // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9719        // promise callbacks queued behind it — the one ordering difference a
9720        // library scheduling work from a `.then` can actually observe.
9721        if !self.draining_micro {
9722            if let Some(t) = self.nextticks.pop_front() {
9723                return Some(t);
9724            }
9725        }
9726        if let Some(t) = self.microtasks.pop_front() {
9727            // Stay in the microtask phase until this queue is exhausted.
9728            self.draining_micro = !self.microtasks.is_empty();
9729            return Some(t);
9730        }
9731        self.draining_micro = false;
9732        self.nextticks.pop_front()
9733    }
9734    fn has_microtasks(&self) -> bool {
9735        !self.nextticks.is_empty() || !self.microtasks.is_empty()
9736    }
9737    /// Whether any pending timer is *referenced* — the timer half of Node's
9738    /// handle count. Only these keep the loop alive; unref'd timers still fire
9739    /// while something else holds the loop open, but never hold it themselves.
9740    fn has_refed_macrotasks(&self) -> bool {
9741        self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9742    }
9743    /// Whether any pending timer repeats. A repeating timer cannot run on the
9744    /// virtual clock: virtual time never advances, so the interval would re-arm
9745    /// at the same instant forever, spinning a core and starving every
9746    /// longer-delay timer behind it. Its presence forces the real clock.
9747    fn has_pending_interval(&self) -> bool {
9748        self.macrotasks
9749            .iter()
9750            .any(|t| !t.cancelled && t.interval.is_some())
9751    }
9752}
9753
9754/// Drive the event loop to quiescence.
9755///
9756/// **Liveness** is Node's handle count: the loop runs while a microtask is
9757/// pending, an open handle is registered (a listening server, a live socket, an
9758/// in-flight async op), or a *referenced* timer is still pending. That last term
9759/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9760/// does in Node — the interval re-arms itself, so a ref'd timer is always
9761/// pending and the loop never reaches its exit condition.
9762///
9763/// Two **clock regimes**, selected per iteration:
9764///
9765/// - **Virtual clock** (no open handles and no repeating timer): the original
9766///   deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9767///   real waiting. Parity output and test speed for ordinary `setTimeout`
9768///   scripts are unchanged.
9769/// - **Real clock** (an open handle, or any pending interval): fire every timer
9770///   whose wall-clock deadline has passed, then BLOCK on the I/O channel
9771///   (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9772///   timers) and run the received `IoTask` on the main thread. The host keeps
9773///   its own `Sender`, so `recv` never disconnects while the process should stay
9774///   alive.
9775///
9776///   A repeating timer *must* take this path: virtual time never advances, so an
9777///   interval on the virtual clock would re-fire at the same instant forever,
9778///   spinning a core and starving every longer-delay timer behind it.
9779///
9780/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9781pub fn run_event_loop() -> Result<(), String> {
9782    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9783    // host borrow); restore it afterward so a re-entrant run reuses the channel.
9784    let rx = with_host(|h| h.io_rx.take());
9785    let result = drive_event_loop(rx.as_ref());
9786    with_host(|h| h.io_rx = rx);
9787    result
9788}
9789
9790fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9791    loop {
9792        // 1) Exhaust the microtask queue (nextTick before promise reactions),
9793        //    then report anything that rejected with nobody watching.
9794        while let Some(task) = with_host(|h| h.next_microtask()) {
9795            task.run()?;
9796        }
9797        check_unhandled_rejections()?;
9798
9799        // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9800        //    process exits, dropping any unref'd timers still pending — which is
9801        //    why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9802        //    timer behind a ref'd one does.
9803        let alive =
9804            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9805        if !alive {
9806            break;
9807        }
9808
9809        // 3) Pick the clock regime for this turn.
9810        let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9811        if virtual_clock {
9812            // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9813            match with_host(|h| h.pop_next_timer()) {
9814                Some(t) => fire_timer(t)?,
9815                // Unreachable while `alive` holds (a ref'd timer must exist),
9816                // but exiting is the safe reading of "nothing left to run".
9817                None => break,
9818            }
9819            continue;
9820        }
9821
9822        // ── real-clock / blocking-I/O regime ─────────────────────────────────
9823        let now = Instant::now();
9824        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9825            fire_timer(t)?;
9826            continue; // re-drain microtasks, re-check deadlines
9827        }
9828        // Nothing due and no pending microtasks: block for the next I/O event,
9829        // bounded by the soonest timer deadline so due timers still fire on time.
9830        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9831        let timeout = with_host(|h| h.next_timer_timeout(now));
9832        let recv = match timeout {
9833            Some(d) => rx.recv_timeout(d),
9834            None => rx
9835                .recv()
9836                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9837        };
9838        match recv {
9839            Ok(task) => task()?,
9840            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9841            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9842        }
9843    }
9844    Ok(())
9845}
9846
9847/// Run one due timer's callback, first re-arming it if it repeats.
9848///
9849/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9850/// issued from inside that callback cancels the next occurrence. Re-arming
9851/// afterwards would leave the interval absent from the queue for the duration of
9852/// its own callback, so the `clear` would match nothing and the freshly pushed
9853/// entry would resurrect an interval the program had just stopped.
9854fn fire_timer(t: Timer) -> Result<(), String> {
9855    if let Some(period) = t.interval {
9856        with_host(|h| h.rearm_timer(&t, period));
9857    }
9858    invoke(&t.callback, t.args, None)?;
9859    Ok(())
9860}
9861
9862// ── async functions & promise resolution (native) ────────────────────────────
9863
9864/// Drive a freshly-built async coroutine and return its result promise.
9865fn run_async(gen: Value) -> Value {
9866    let result = with_host(|h| h.new_promise());
9867    let rid = with_host(|h| h.promise_id(&result).unwrap());
9868    drive_async(gen, rid, Value::Undef);
9869    result
9870}
9871
9872/// Resume an async coroutine one step, wiring `await` continuations to promise
9873/// settlement.
9874fn drive_async(gen: Value, rid: u32, send: Value) {
9875    match gen_resume(&gen, send) {
9876        Ok(GenStep::Yield(awaited)) => {
9877            let ap = promise_of(&awaited);
9878            let aid = with_host(|h| h.promise_id(&ap).unwrap());
9879            let gen2 = gen.clone();
9880            subscribe_native(
9881                aid,
9882                Box::new(move |state, val| {
9883                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
9884                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9885                    let tag = if state == PromiseState::Rejected {
9886                        1.0
9887                    } else {
9888                        0.0
9889                    };
9890                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9891                    drive_async(gen2, rid, packet);
9892                    Ok(())
9893                }),
9894            );
9895        }
9896        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9897        Err(e) => {
9898            let ev = take_exc_or_error(&e);
9899            reject_promise_val(rid, ev);
9900        }
9901    }
9902}
9903
9904/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9905/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9906pub fn await_value(awaited: Value) -> Result<Value, String> {
9907    // Inside an `async function*`, `await` and `yield` share one coroutine
9908    // yielder, so an awaited value has to be tagged or the driver would hand it
9909    // to the consumer as if the body had yielded it.
9910    let awaited = match CUR_GEN.with(|c| c.get()) {
9911        Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9912            let mut m = IndexMap::new();
9913            m.insert(AWAIT_MARKER.to_string(), awaited);
9914            h.new_object(m)
9915        }),
9916        _ => awaited,
9917    };
9918    let packet = gen_yield(awaited)?;
9919    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9920    let tag = items
9921        .first()
9922        .map(|v| with_host(|h| h.to_number(v)))
9923        .unwrap_or(0.0);
9924    let val = items.get(1).cloned().unwrap_or(Value::Undef);
9925    if tag == 1.0 {
9926        with_host(|h| h.exc = Some(val.clone()));
9927        Err(with_host(|h| crate::builtins::error_string(h, &val)))
9928    } else {
9929        Ok(val)
9930    }
9931}
9932
9933/// Hidden key marking an `await` suspension inside an async generator.
9934const AWAIT_MARKER: &str = "@@await";
9935
9936/// The operand of an `await` suspension, or `None` for a real `yield`.
9937fn await_marker(v: &Value) -> Option<Value> {
9938    with_host(|h| match h.get(v) {
9939        Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9940        _ => None,
9941    })
9942}
9943
9944/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9945/// hand back the promise its `{value, done}` record (or rejection) will settle.
9946///
9947/// All three of `.next`, `.return` and `.throw` come through here, so a request
9948/// never resumes the body while an earlier one is still suspended on an
9949/// internal `await`.
9950pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9951    let step = with_host(|h| h.new_promise());
9952    let sid = with_host(|h| h.promise_id(&step).unwrap());
9953    let id = match with_host(|h| match h.get(gen) {
9954        Some(JsObj::Generator { id }) => Some(*id),
9955        _ => None,
9956    }) {
9957        Some(id) => id,
9958        None => return step,
9959    };
9960    with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9961    pump_async_gen(gen.clone(), id);
9962    step
9963}
9964
9965/// One `.next(v)` of an `async function*`.
9966pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9967    async_gen_enqueue(gen, GenReq::Next(send))
9968}
9969
9970/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9971/// already in flight (the body may only be resumed by one request at a time).
9972fn pump_async_gen(gen: Value, id: u32) {
9973    if with_host(|h| h.generators[id as usize].running) {
9974        return;
9975    }
9976    let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9977        return;
9978    };
9979    with_host(|h| h.generators[id as usize].running = true);
9980    start_async_gen_req(gen, sid, req);
9981}
9982
9983/// Begin one queued request: resume the body with the completion it carries,
9984/// then hand the outcome to the shared continuation.
9985///
9986/// A RETURN completion always Awaits its value before the body sees it — via
9987/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9988/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9989/// when it is not yet started or already completed. So a `.return()` settles one
9990/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9991/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9992/// `.return()` overtake the reactions of the `.next()` it followed.
9993fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9994    if matches!(req, GenReq::Return(_)) {
9995        with_host(|h| {
9996            h.queue_micro_native(Box::new(move || {
9997                resume_async_gen_req(gen, sid, req);
9998                Ok(())
9999            }))
10000        });
10001        return;
10002    }
10003    resume_async_gen_req(gen, sid, req);
10004}
10005
10006/// Deliver a queued completion to the body and settle its step promise.
10007fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
10008    let step = match req {
10009        GenReq::Next(v) => gen_resume(&gen, v),
10010        GenReq::Return(v) => gen_return(&gen, v),
10011        GenReq::Throw(e) => gen_throw(&gen, e),
10012    };
10013    settle_async_gen_step(gen, sid, step);
10014}
10015
10016/// One request has settled: release the body and start the next queued request.
10017fn finish_async_gen_step(gen: Value, id: u32) {
10018    with_host(|h| h.generators[id as usize].running = false);
10019    pump_async_gen(gen, id);
10020}
10021
10022/// Whether `v` is an `async function*` object (its `.next()` yields promises).
10023pub fn is_async_generator(v: &Value) -> bool {
10024    let id = match with_host(|h| match h.get(v) {
10025        Some(JsObj::Generator { id }) => Some(*id),
10026        _ => None,
10027    }) {
10028        Some(id) => id,
10029        None => return false,
10030    };
10031    with_host(|h| h.generators[id as usize].async_gen)
10032}
10033
10034/// A `{ value, done }` iterator-result object.
10035fn iter_record(value: Value, done: bool) -> Value {
10036    with_host(|h| {
10037        let mut m = IndexMap::new();
10038        m.insert("value".to_string(), value);
10039        m.insert("done".to_string(), Value::Bool(done));
10040        h.new_object(m)
10041    })
10042}
10043
10044/// Resume a request that was suspended on an internal `await` (always a normal
10045/// completion — the awaited promise's outcome rides in `packet`).
10046fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
10047    let step = gen_resume(&gen, packet);
10048    settle_async_gen_step(gen, sid, step);
10049}
10050
10051/// Turn one body resumption into a settled step promise: transparently re-drive
10052/// internal `await` suspensions, and settle on the first REAL yield or on the
10053/// body's completion. Shared by the initial resume of a queued request and by
10054/// every await-resumption of it.
10055fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
10056    let id = match with_host(|h| match h.get(&gen) {
10057        Some(JsObj::Generator { id }) => Some(*id),
10058        _ => None,
10059    }) {
10060        Some(id) => id,
10061        None => return,
10062    };
10063    match step {
10064        Ok(GenStep::Yield(v)) => match await_marker(&v) {
10065            Some(awaited) => {
10066                // An internal `await`: settle it, then resume the body. The
10067                // request stays in flight across the suspension.
10068                let ap = promise_of(&awaited);
10069                let aid = with_host(|h| h.promise_id(&ap).unwrap());
10070                subscribe_native(
10071                    aid,
10072                    Box::new(move |state, val| {
10073                        let tag = if state == PromiseState::Rejected {
10074                            1.0
10075                        } else {
10076                            0.0
10077                        };
10078                        let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
10079                        drive_async_gen(gen.clone(), sid, packet);
10080                        Ok(())
10081                    }),
10082                );
10083            }
10084            // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
10085            // AWAITED before the step promise settles, so `yield somePromise`
10086            // hands the consumer the RESOLVED value (and costs its microtask).
10087            None => {
10088                let yp = promise_of(&v);
10089                let yid = with_host(|h| h.promise_id(&yp).unwrap());
10090                subscribe_native(
10091                    yid,
10092                    Box::new(move |state, val| {
10093                        if state == PromiseState::Rejected {
10094                            reject_promise_val(sid, val);
10095                        } else {
10096                            resolve_promise_val(sid, iter_record(val, false));
10097                        }
10098                        finish_async_gen_step(gen.clone(), id);
10099                        Ok(())
10100                    }),
10101                );
10102            }
10103        },
10104        Ok(GenStep::Done(v)) => {
10105            resolve_promise_val(sid, iter_record(v, true));
10106            finish_async_gen_step(gen, id);
10107        }
10108        Err(e) => {
10109            let ev = take_exc_or_error(&e);
10110            reject_promise_val(sid, ev);
10111            finish_async_gen_step(gen, id);
10112        }
10113    }
10114}
10115
10116/// A promise for `v`: `v` itself if it is already a promise, else a promise
10117/// resolved with `v`.
10118pub fn promise_of(v: &Value) -> Value {
10119    if with_host(|h| h.promise_id(v)).is_some() {
10120        return v.clone();
10121    }
10122    let p = with_host(|h| h.new_promise());
10123    let id = with_host(|h| h.promise_id(&p).unwrap());
10124    resolve_promise_val(id, v.clone());
10125    p
10126}
10127
10128/// Register a native reaction on promise `id` (schedules immediately if already
10129/// settled).
10130pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
10131    // A native continuation (`await`, promise adoption, `for await`) observes a
10132    // rejection exactly as a `.catch` does, so it is not "unhandled".
10133    with_host(|h| h.promise_mark_handled(id));
10134    let state = with_host(|h| h.promise_state(id));
10135    if state == PromiseState::Pending {
10136        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
10137    } else {
10138        let val = with_host(|h| h.promise_value(id));
10139        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
10140    }
10141}
10142
10143/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
10144/// else fulfill with it.
10145pub fn resolve_promise_val(id: u32, value: Value) {
10146    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10147        return;
10148    }
10149    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10150        if vid == id {
10151            // Resolving a promise with itself → reject with a TypeError.
10152            let e = with_host(|h| {
10153                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10154            });
10155            reject_promise_val(id, e);
10156            return;
10157        }
10158        // A native promise is still a thenable, so the spec routes it through
10159        // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10160        // is even registered. (`await` does NOT pay this: V8's await optimization
10161        // subscribes to a native promise directly, which `await_value` mirrors.)
10162        with_host(|h| {
10163            h.queue_micro_native(Box::new(move || {
10164                subscribe_native(
10165                    vid,
10166                    Box::new(move |state, val| {
10167                        with_host(|h| h.settle_promise(id, state, val.clone()));
10168                        schedule_reactions(id);
10169                        Ok(())
10170                    }),
10171                );
10172                Ok(())
10173            }))
10174        });
10175        return;
10176    }
10177    // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10178    // through a dedicated job — the promise adopts what `then` reports, it is
10179    // never fulfilled WITH the thenable itself.
10180    if let Some(then) = thenable_then(&value) {
10181        with_host(|h| {
10182            h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10183        });
10184        return;
10185    }
10186    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10187    schedule_reactions(id);
10188}
10189
10190/// `value.then` if `value` is an object with a callable `then` — the test that
10191/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10192fn thenable_then(value: &Value) -> Option<Value> {
10193    // A PROXY is not a plain object and supplies `then` through its `get` trap,
10194    // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10195    // WITH the proxy instead of adopting it.
10196    if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10197        return protocol_lookup(value, "then")
10198            .ok()
10199            .flatten()
10200            .filter(|f| with_host(|h| is_callable(h, f)));
10201    }
10202    if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10203        return None;
10204    }
10205    let then = with_host(|h| lookup_chain(h, value, "then"))?;
10206    with_host(|h| is_callable(h, &then)).then_some(then)
10207}
10208
10209/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10210/// reject continuations and let it settle us. A throw out of `then` rejects.
10211fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10212    let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10213    let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10214    if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10215        let ev = take_exc_or_error(&e);
10216        reject_promise_val(id, ev);
10217    }
10218    Ok(())
10219}
10220
10221pub fn reject_promise_val(id: u32, value: Value) {
10222    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10223        return;
10224    }
10225    with_host(|h| {
10226        h.settle_promise(id, PromiseState::Rejected, value);
10227        h.pending_rejections.push(id);
10228    });
10229    schedule_reactions(id);
10230}
10231
10232/// Report every promise that settled rejected since the last checkpoint and
10233/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10234/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10235/// `process.on('unhandledRejection')` listener takes it.
10236fn check_unhandled_rejections() -> Result<(), String> {
10237    loop {
10238        let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10239        if ids.is_empty() {
10240            return Ok(());
10241        }
10242        for id in ids {
10243            let unhandled = with_host(|h| {
10244                h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10245            });
10246            if !unhandled {
10247                continue;
10248            }
10249            // Report each promise at most once, however many checkpoints pass.
10250            with_host(|h| h.promise_mark_handled(id));
10251            let val = with_host(|h| h.promise_value(id));
10252            let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10253            if listeners.is_empty() {
10254                let msg = with_host(|h| crate::builtins::error_string(h, &val));
10255                with_host(|h| h.exc = Some(val));
10256                return Err(msg);
10257            }
10258            let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10259            for f in listeners {
10260                invoke(&f, vec![val.clone(), promise.clone()], None)?;
10261            }
10262        }
10263    }
10264}
10265
10266/// Drain a settled promise's reactions into microtasks.
10267fn schedule_reactions(id: u32) {
10268    let reactions = with_host(|h| h.take_reactions(id));
10269    let state = with_host(|h| h.promise_state(id));
10270    let value = with_host(|h| h.promise_value(id));
10271    for r in reactions {
10272        let value = value.clone();
10273        match r {
10274            PromiseReaction::Native(f) => {
10275                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10276            }
10277            PromiseReaction::Js {
10278                on_ful,
10279                on_rej,
10280                result,
10281            } => {
10282                with_host(|h| {
10283                    h.queue_micro_native(Box::new(move || {
10284                        run_js_reaction(state, value, on_ful, on_rej, result)
10285                    }))
10286                });
10287            }
10288        }
10289    }
10290}
10291
10292/// Run a `.then` reaction: call the appropriate handler and settle the result
10293/// promise with its outcome (or pass through if there is no handler).
10294fn run_js_reaction(
10295    state: PromiseState,
10296    value: Value,
10297    on_ful: Value,
10298    on_rej: Value,
10299    result: Value,
10300) -> Result<(), String> {
10301    let rid = match with_host(|h| h.promise_id(&result)) {
10302        Some(i) => i,
10303        None => return Ok(()),
10304    };
10305    let handler = if state == PromiseState::Rejected {
10306        on_rej
10307    } else {
10308        on_ful
10309    };
10310    if with_host(|h| is_callable(h, &handler)) {
10311        match invoke(&handler, vec![value], None) {
10312            Ok(r) => resolve_promise_val(rid, r),
10313            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10314        }
10315    } else if state == PromiseState::Rejected {
10316        reject_promise_val(rid, value);
10317    } else {
10318        resolve_promise_val(rid, value);
10319    }
10320    Ok(())
10321}
10322
10323/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10324/// synthesized `Error` from the internal message.
10325pub fn take_exc_or_error(e: &str) -> Value {
10326    with_host(|h| {
10327        h.error.take();
10328        h.exc
10329            .take()
10330            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10331    })
10332}
10333
10334/// Register a user `.then` reaction (JS handlers + result promise).
10335pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10336    let id = match with_host(|h| h.promise_id(p)) {
10337        Some(i) => i,
10338        None => return Value::Undef,
10339    };
10340    with_host(|h| h.promise_mark_handled(id));
10341    // The result is built with the RECEIVER's species, so a subclass promise
10342    // stays a subclass promise through a `.then` chain.
10343    let result = match crate::builtins::promise_species_from(p) {
10344        Ok(Some(sp)) => sp,
10345        _ => with_host(|h| h.new_promise()),
10346    };
10347    let reaction = PromiseReaction::Js {
10348        on_ful,
10349        on_rej,
10350        result: result.clone(),
10351    };
10352    let state = with_host(|h| h.promise_state(id));
10353    if state == PromiseState::Pending {
10354        with_host(|h| h.add_reaction(id, reaction));
10355    } else {
10356        let value = with_host(|h| h.promise_value(id));
10357        if let PromiseReaction::Js {
10358            on_ful,
10359            on_rej,
10360            result,
10361        } = reaction
10362        {
10363            with_host(|h| {
10364                h.queue_micro_native(Box::new(move || {
10365                    run_js_reaction(state, value, on_ful, on_rej, result)
10366                }))
10367            });
10368        }
10369    }
10370    result
10371}
10372
10373/// The ReferenceError for touching `this` in a derived constructor before
10374/// `super()` — or returning from one without calling it.
10375pub fn this_before_super_error() -> String {
10376    "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10377}
10378
10379/// The intrinsic a builtin name denotes. Two property paths that the spec
10380/// defines as the SAME function object compare `===`: `Number.parseInt` is
10381/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10382/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10383fn builtin_identity(name: &str) -> &str {
10384    match name {
10385        "Number.parseInt" => "parseInt",
10386        "Number.parseFloat" => "parseFloat",
10387        _ => name,
10388    }
10389}