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}
111
112/// Per-call-site callee SOURCE TEXT, for the `TypeError` a failed call raises.
113///
114/// V8 reports the callee the way the source wrote it — `z.f is not a function`,
115/// not `f is not a function` — by re-printing the AST of the call it was
116/// evaluating. The text is therefore a static property of the SITE, so the
117/// compiler records it once per call op and nothing is carried at run time: the
118/// table is consulted only on the error path.
119///
120/// Keyed by the chunk's `op_hash` (which `ChunkBuilder::build` computes anyway)
121/// paired with the op index. `op_hash` covers the op vector but not the name
122/// pool, so two chunks that compile to the same ops with different names share a
123/// key; the consequence is confined to which receiver text an error message
124/// names, never to what a program does.
125mod call_sites {
126    use std::cell::RefCell;
127
128    thread_local! {
129        pub(super) static SITES: RefCell<rustc_hash::FxHashMap<(u64, usize), String>> =
130            RefCell::new(rustc_hash::FxHashMap::default());
131    }
132
133    /// Record every call site of a freshly built chunk.
134    pub fn register(op_hash: u64, sites: Vec<(usize, String)>) {
135        if sites.is_empty() {
136            return;
137        }
138        SITES.with(|m| {
139            let mut m = m.borrow_mut();
140            for (ip, text) in sites {
141                m.insert((op_hash, ip), text);
142            }
143        });
144    }
145
146    /// The callee text recorded for the op at `ip` of the chunk `op_hash`.
147    pub fn text(op_hash: u64, ip: usize) -> Option<String> {
148        SITES.with(|m| m.borrow().get(&(op_hash, ip)).cloned())
149    }
150
151    pub fn clear() {
152        SITES.with(|m| m.borrow_mut().clear());
153    }
154}
155
156pub use call_sites::{clear as clear_call_sites, register as register_call_sites};
157
158/// How many `for…of` / `yield*` iterators are parked on the VM stack at each
159/// `yield` op, recorded by the compiler the same way callee text is.
160///
161/// A `.return()`/`.throw()` injected at a suspension point halts the generator's
162/// chunk outright, which jumps past the loop exits that would have closed those
163/// iterators — so the halt path has to close them itself, and this is how it
164/// knows how many are there and that they are the top of the stack.
165mod yield_sites {
166    use std::cell::RefCell;
167
168    thread_local! {
169        pub(super) static DEPTHS: RefCell<rustc_hash::FxHashMap<(u64, usize), usize>> =
170            RefCell::new(rustc_hash::FxHashMap::default());
171    }
172
173    pub fn register(op_hash: u64, sites: Vec<(usize, usize)>) {
174        if sites.is_empty() {
175            return;
176        }
177        DEPTHS.with(|m| {
178            let mut m = m.borrow_mut();
179            for (ip, depth) in sites {
180                m.insert((op_hash, ip), depth);
181            }
182        });
183    }
184
185    pub fn depth(op_hash: u64, ip: usize) -> usize {
186        DEPTHS.with(|m| m.borrow().get(&(op_hash, ip)).copied().unwrap_or(0))
187    }
188
189    pub fn clear() {
190        DEPTHS.with(|m| m.borrow_mut().clear());
191    }
192}
193
194pub use yield_sites::{clear as clear_yield_sites, register as register_yield_sites};
195
196/// Every call site and yield site registered so far, as the cache stores them:
197/// `(op_hash, ip)` keys with their recorded value.
198///
199/// The tables are built by the COMPILER (`finish_chunk`), so a run that loads a
200/// program from the bytecode cache never fills them — and everything that reads
201/// them silently degrades: a generator's parked `for…of`/`yield*` iterators are
202/// not closed on an injected `.return()`, so their `finally` never runs, and a
203/// `TypeError` loses the callee's source text. Storing them alongside the
204/// program is what makes a cache hit behave like a compile.
205pub type SiteTables = (Vec<((u64, usize), String)>, Vec<((u64, usize), usize)>);
206
207/// Snapshot both registries.
208pub fn site_tables() -> SiteTables {
209    let calls = call_sites::SITES.with(|m| {
210        m.borrow()
211            .iter()
212            .map(|(k, v)| (*k, v.clone()))
213            .collect::<Vec<_>>()
214    });
215    let yields =
216        yield_sites::DEPTHS.with(|m| m.borrow().iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>());
217    (calls, yields)
218}
219
220/// Put a snapshot back — what a cache hit does in place of compiling.
221pub fn restore_site_tables(t: &SiteTables) {
222    call_sites::SITES.with(|m| {
223        let mut m = m.borrow_mut();
224        for (k, v) in &t.0 {
225            m.insert(*k, v.clone());
226        }
227    });
228    yield_sites::DEPTHS.with(|m| {
229        let mut m = m.borrow_mut();
230        for (k, v) in &t.1 {
231            m.insert(*k, *v);
232        }
233    });
234}
235
236/// The number of loop iterators parked on the stack at the op currently
237/// executing, for the abrupt-completion close in `b_yield`.
238pub fn parked_iters(vm: &fusevm::VM) -> usize {
239    yield_sites::depth(vm.chunk.op_hash, vm.ip.saturating_sub(1))
240}
241
242/// Rewrite a `<subject> is not a function` / `is not a constructor` message with
243/// the SOURCE TEXT of the callee at the currently executing op, as V8 does.
244///
245/// `subject` is what the raising code named — the method name, or the callee's
246/// rendered value. The message's own subject must END WITH it, which is the
247/// guard that keeps an unrelated error raised deeper inside a native method from
248/// being relabelled with this call's text. (A native dispatcher may prefix its
249/// own receiver word, e.g. `map.get is not a function`, so the whole subject is
250/// replaced rather than trimmed by length.)
251///
252/// Returns the message unchanged when no site was recorded, so a shape the
253/// printer declines to print keeps the old wording rather than an invented one.
254pub fn name_call_site(vm: &fusevm::VM, subject: &str, msg: String) -> String {
255    for tail in [" is not a function", " is not a constructor"] {
256        let Some(head) = msg.strip_suffix(tail) else {
257            continue;
258        };
259        // The prefix is the error class (`TypeError: `); the rest is the subject.
260        let (prefix, found) = match head.rfind(": ") {
261            Some(i) => (&head[..i + 2], &head[i + 2..]),
262            None => ("", head),
263        };
264        if !found.ends_with(subject) {
265            return msg;
266        }
267        // `vm.ip` has already advanced past the op being executed.
268        let Some(text) = call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1)) else {
269            return msg;
270        };
271        return format!("{prefix}{text}{tail}");
272    }
273    msg
274}
275
276/// `SIG_UNWIND` scope tags: what the emitting site is nested in.
277pub mod unwind {
278    /// No enclosing loop in this chunk — any pending signal propagates outward.
279    pub const NO_LOOP: &str = "";
280    /// An enclosing UNLABELED loop in this chunk.
281    pub const PLAIN_LOOP: &str = "\u{0}";
282    /// `SIG_UNWIND` result codes.
283    pub const NONE: i64 = 0;
284    pub const BREAK: i64 = 1;
285    pub const CONTINUE: i64 = 2;
286}
287
288/// `DEF_MEMBER` member-kind tags.
289pub mod member {
290    pub const METHOD: i64 = 0;
291    pub const GET: i64 = 1;
292    pub const SET: i64 = 2;
293    /// A static FIELD (`static x = 1`), which is a data property of the
294    /// constructor rather than a method. Only distinguished from `METHOD` for a
295    /// PRIVATE name, where the declaration must install the private element
296    /// without tripping the brand check an ordinary write to `#x` gets — and
297    /// where node's brand-check message words a field differently from a method.
298    pub const STATIC_FIELD: i64 = 3;
299}
300
301/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
302pub mod binop {
303    pub const BITAND: i64 = 0;
304    pub const BITOR: i64 = 1;
305    pub const BITXOR: i64 = 2;
306    pub const SHL: i64 = 3;
307    pub const SHR: i64 = 4;
308    pub const USHR: i64 = 5;
309}
310
311/// Unary op tags carried by `ops::UNARY`.
312pub mod unop {
313    pub const POS: i64 = 0; // unary +
314    pub const BITNOT: i64 = 1; // ~
315}
316
317// ── heap objects ───────────────────────────────────────────────────────────
318
319/// A compiled function template: parameter shape + body chunk. Shared by every
320/// closure created from the same function/arrow.
321#[derive(Clone, serde::Serialize, serde::Deserialize)]
322pub struct FuncDef {
323    pub name: String,
324    /// Parameter binding templates (destructuring lowered by the compiler into
325    /// the body prologue; here we only track the simple arg slots).
326    pub params: Vec<ParamSlot>,
327    pub chunk: Chunk,
328    pub is_arrow: bool,
329    /// True for a `function*`/`*method`/generator arrow: calling it builds a
330    /// suspended generator instead of running the body.
331    pub is_generator: bool,
332    /// True for an `async` function/method/arrow: calling it drives a coroutine
333    /// and returns a Promise; `await` inside suspends via the same yielder.
334    pub is_async: bool,
335    /// True for a MethodDefinition (`{ m(){} }`, a class method/accessor). A
336    /// non-generator method is not a constructor, so it owns no `prototype`.
337    #[serde(default)]
338    pub is_method: bool,
339    /// True for a NAMED function *expression* (`const f = function fact(n) {…}`):
340    /// the closure gets an extra environment binding its own name to itself, so
341    /// the body can recurse through that name even when the outer binding differs.
342    #[serde(default)]
343    pub self_name: bool,
344}
345
346/// One parameter slot. `name` is the simple bound name; a destructuring pattern
347/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
348#[derive(Clone, serde::Serialize, serde::Deserialize)]
349pub struct ParamSlot {
350    pub name: String,
351    /// True for the `...rest` collector.
352    pub rest: bool,
353    /// True if this slot has a default expression (applied in the body prologue).
354    pub has_default: bool,
355}
356
357/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
358/// current scope.
359#[derive(Clone, serde::Serialize, serde::Deserialize)]
360pub struct TryDef {
361    pub block: Chunk,
362    /// `(catch_param_name, catch_body)`.
363    pub handler: Option<(Option<String>, Chunk)>,
364    pub finalizer: Option<Chunk>,
365}
366
367/// A live closure value.
368#[derive(Clone)]
369pub struct FuncVal {
370    pub def_id: usize,
371    /// Captured lexical environment (enclosing scope chain), for free vars.
372    pub env: Option<Env>,
373    /// `this` captured at definition time (arrow functions).
374    pub this: Option<Value>,
375    pub is_arrow: bool,
376    /// The owning class name for a method (drives `super` resolution). `None` for
377    /// plain functions/arrows.
378    pub home_class: Option<String>,
379}
380
381/// A heap object.
382#[derive(Clone)]
383pub enum JsObj {
384    Str(String),
385    Array(Vec<Value>),
386    Object(IndexMap<String, Value>),
387    Func(FuncVal),
388    /// A first-class reference to a builtin function or namespace
389    /// (`console.log`, `Math`, `parseInt`).
390    Builtin(String),
391    /// A bound method value (`obj.method` captured then called): dispatches
392    /// through `call_method(recv, name, args)` when invoked.
393    BoundMethod {
394        recv: Value,
395        name: String,
396    },
397    /// The single canonical `null`.
398    Null,
399    /// A live iterator over a sequence, with a cursor.
400    Iter {
401        items: Vec<Value>,
402        idx: usize,
403    },
404    /// A bound function (`fn.bind(thisArg, ...preargs)`).
405    BoundFunc {
406        target: Value,
407        this: Value,
408        args: Vec<Value>,
409    },
410    /// A class constructor value: the runtime object produced by a `class`.
411    Class(ClassVal),
412    /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
413    /// key (shared) vs a fresh `Symbol()`.
414    Symbol {
415        desc: Option<String>,
416        id: u64,
417    },
418    /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
419    Map {
420        entries: IndexMap<MapKey, (Value, Value)>,
421        weak: bool,
422    },
423    /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
424    Set {
425        entries: IndexMap<MapKey, Value>,
426        weak: bool,
427    },
428    /// A live generator, backed by a stackful `corosensei` coroutine in
429    /// `JsHost.generators`.
430    Generator {
431        id: u32,
432    },
433    /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
434    Promise {
435        id: u32,
436    },
437    /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
438    BigInt(num_bigint::BigInt),
439    /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
440    RegExp(Box<RegExpObj>),
441    /// A `Proxy`: every essential internal method is diverted to `handler`'s
442    /// traps (see `crate::proxy`). `revoked` is set by the thunk
443    /// `Proxy.revocable` hands back, after which every operation throws.
444    Proxy {
445        target: Value,
446        handler: Value,
447        revoked: bool,
448    },
449}
450
451/// Which variant a heap object is, carrying none of its contents.
452///
453/// Property access has to pick a branch by variant, but the code inside a branch
454/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
455/// hold a `&JsObj` borrow across the match. The way out used to be
456/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
457/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
458/// property read O(len) and any loop over a collection O(n^2). This type is the
459/// same discriminant with nothing attached, so the probe is O(1) and each branch
460/// re-borrows for only the one field it actually needs.
461/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
462/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
463/// under the sentinel string `@@<name>` (`property_key`) so the internal
464/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
465/// table walk. Symbols V8 defines but node-js does not act on are deliberately
466/// absent: a symbol that reads back while the operator it names ignores it would
467/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
468pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
469    "iterator",
470    "asyncIterator",
471    "toPrimitive",
472    "toStringTag",
473    "hasInstance",
474];
475
476/// Whether the internal key `k` came from a SYMBOL used as a property key
477/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
478/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
479/// is an observable JavaScript property.
480pub fn is_symbol_key(k: &str) -> bool {
481    match k.strip_prefix("@@") {
482        Some(rest) => rest
483            .strip_prefix("sym:")
484            .map(|i| i.parse::<u64>().is_ok())
485            .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
486        None => false,
487    }
488}
489
490#[derive(Clone, Copy, PartialEq, Eq, Debug)]
491pub enum ObjKind {
492    Str,
493    Array,
494    Object,
495    Func,
496    Builtin,
497    BoundMethod,
498    Null,
499    Iter,
500    BoundFunc,
501    Class,
502    Symbol,
503    Map,
504    Set,
505    Generator,
506    Promise,
507    BigInt,
508    RegExp,
509    Proxy,
510}
511
512impl JsObj {
513    /// This object's variant, without touching its contents.
514    pub fn kind(&self) -> ObjKind {
515        match self {
516            JsObj::Str(_) => ObjKind::Str,
517            JsObj::Array(_) => ObjKind::Array,
518            JsObj::Object(_) => ObjKind::Object,
519            JsObj::Func(_) => ObjKind::Func,
520            JsObj::Builtin(_) => ObjKind::Builtin,
521            JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
522            JsObj::Null => ObjKind::Null,
523            JsObj::Iter { .. } => ObjKind::Iter,
524            JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
525            JsObj::Class(_) => ObjKind::Class,
526            JsObj::Symbol { .. } => ObjKind::Symbol,
527            JsObj::Map { .. } => ObjKind::Map,
528            JsObj::Set { .. } => ObjKind::Set,
529            JsObj::Generator { .. } => ObjKind::Generator,
530            JsObj::Promise { .. } => ObjKind::Promise,
531            JsObj::BigInt(_) => ObjKind::BigInt,
532            JsObj::RegExp(_) => ObjKind::RegExp,
533            JsObj::Proxy { .. } => ObjKind::Proxy,
534        }
535    }
536}
537
538/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
539/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
540/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
541/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
542#[derive(Clone)]
543pub struct RegExpObj {
544    /// The translated regex. Construction of a pattern fancy-regex still cannot
545    /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
546    /// `RegExpObj` always holds a compiled engine.
547    ///
548    /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
549    /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
550    /// per-object mutable state — while the compiled engine behind it is
551    /// immutable and identical every time. See `regexp::compiled`.
552    pub re: std::rc::Rc<fancy_regex::Regex>,
553    pub source: String,
554    pub flags: String,
555    pub global: bool,
556    pub ignore_case: bool,
557    pub multiline: bool,
558    pub dot_all: bool,
559    pub sticky: bool,
560    pub unicode: bool,
561    /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
562    /// `g`/`y` flags. The newtype keeps it from being confused with the regex
563    /// engine's byte offsets, which are the same shape and differ off the BMP.
564    pub last_index: crate::utf16::U16Index,
565}
566
567/// A Promise's settled state and pending reactions.
568pub struct PromiseCell {
569    pub state: PromiseState,
570    pub value: Value,
571    /// Reactions registered while still pending; drained (as microtasks) on
572    /// settle.
573    pub reactions: Vec<PromiseReaction>,
574    /// True once a rejection has been observed by a handler (`.then`/`.catch`),
575    /// so the loop doesn't report it as unhandled.
576    pub handled: bool,
577}
578
579/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
580/// a native continuation (Promise chaining / async `await` resumption).
581pub enum PromiseReaction {
582    Js {
583        on_ful: Value,
584        on_rej: Value,
585        result: Value,
586    },
587    Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
588}
589
590#[derive(Default, Clone, Copy, PartialEq, Eq)]
591pub enum PromiseState {
592    #[default]
593    Pending,
594    Fulfilled,
595    Rejected,
596}
597
598/// A live class constructor. The prototype object (holding instance methods) and
599/// the static-side own properties live on the heap; `parent` is the superclass
600/// constructor value (`None` for a base class).
601#[derive(Clone)]
602pub struct ClassVal {
603    pub name: String,
604    /// The constructor function value (a `JsObj::Func`), or `None` for a class
605    /// with only a synthesized default constructor.
606    pub ctor: Option<Value>,
607    pub parent: Option<Value>,
608    /// `C.prototype` — the object instances delegate to.
609    pub proto: Value,
610    /// Static own properties (static methods/fields), plus `name`/`prototype`.
611    pub statics: IndexMap<String, Value>,
612    /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
613    /// per-instance after `super()` (or at construction start for a base class).
614    /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
615    /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
616    /// result — it cannot be re-derived at run time (a field initialised from an
617    /// already-anonymous function held elsewhere must not be renamed).
618    pub fields: Vec<(String, Value, bool)>,
619}
620
621/// The result of resolving `super.name`: a getter to invoke (accessor property)
622/// or a directly-usable value (method / data property).
623pub enum SuperRef {
624    Getter(Value),
625    Data(Value),
626}
627
628/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
629/// `+0` are the same key, primitives compare by value, objects by heap identity.
630#[derive(Clone, PartialEq, Eq, Hash)]
631pub enum MapKey {
632    Undef,
633    Null,
634    Bool(bool),
635    /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
636    Num(u64),
637    /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
638    Big(String),
639    Str(String),
640    /// Heap identity (objects, arrays, functions, symbols).
641    Ref(u32),
642}
643
644// ── environments ─────────────────────────────────────────────────────────────
645
646/// The map behind a scope. Hashing these with `FxHash` instead of the default
647/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
648/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
649pub type VarMap = IndexMap<String, Value>;
650
651/// A local-variable environment, shared (by `Rc`) between a frame and any nested
652/// function that captures it.
653pub struct EnvData {
654    pub vars: VarMap,
655    /// The names in `vars` that were declared `const`, so an assignment to one
656    /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
657    ///
658    /// A separate set rather than a flag inside `VarMap`'s value, because
659    /// `set_name` is a hot path — the common case is an env with NO consts,
660    /// where `is_empty()` settles it without hashing the name a second time.
661    pub consts: rustc_hash::FxHashSet<String>,
662    pub parent: Option<Env>,
663}
664pub type Env = Rc<RefCell<EnvData>>;
665
666/// An accessor property: `(getter, setter)`, either optional.
667pub type Accessor = (Option<Value>, Option<Value>);
668
669/// Prefix of the hidden property-map entry that reserves an accessor's slot in
670/// own-key insertion order (see `set_accessor`).
671pub const ORD_MARKER: &str = "@@ord:";
672
673/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
674/// all-true shape a plain `o.k = v` assignment produces, which is why only
675/// deviations need storing.
676#[derive(Clone, Copy, Debug, PartialEq, Eq)]
677pub struct PropAttrs {
678    pub writable: bool,
679    pub enumerable: bool,
680    pub configurable: bool,
681}
682
683impl Default for PropAttrs {
684    fn default() -> Self {
685        PropAttrs {
686            writable: true,
687            enumerable: true,
688            configurable: true,
689        }
690    }
691}
692
693impl PropAttrs {
694    /// The attribute shape V8 gives an internal-but-inspectable slot such as
695    /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
696    /// readable and replaceable, but never enumerated.
697    pub const HIDDEN: PropAttrs = PropAttrs {
698        writable: true,
699        enumerable: false,
700        configurable: true,
701    };
702}
703
704fn new_env(parent: Option<Env>) -> Env {
705    Rc::new(RefCell::new(EnvData {
706        vars: VarMap::default(),
707        consts: rustc_hash::FxHashSet::default(),
708        parent,
709    }))
710}
711
712/// A fresh empty scope chained under `parent`.
713pub fn child_env(parent: Env) -> Env {
714    new_env(Some(parent))
715}
716
717/// One function activation.
718pub struct Frame {
719    pub env: Env,
720    /// The env this activation started in — the FUNCTION scope. `var` and hoisted
721    /// function declarations bind here no matter how many block scopes are open.
722    pub base_env: Env,
723    pub this_obj: Option<Value>,
724    /// `new.target` for this activation (the constructor when invoked via `new`).
725    pub new_target: Option<Value>,
726    /// The class value owning the running method (drives `super`); `None` outside
727    /// a class method/constructor.
728    pub home_class: Option<Value>,
729    /// Source line the frame is currently executing (updated by the DAP line hook
730    /// under `--dap`; stays 0 on ordinary runs).
731    pub line: u32,
732    /// The function name that owns this frame, for the DAP `stackTrace`; `None`
733    /// for the module frame and anonymous activations.
734    pub owner: Option<String>,
735    /// True ONLY for the program's module frame. A generator/async body runs on a
736    /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
737    /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
738    /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
739    /// across concurrent activations of the same function.
740    pub is_module: bool,
741}
742
743/// A non-local control signal. `Break`/`Continue` carry the optional loop label
744/// and are only raised when the target loop lives in an ENCLOSING chunk (a
745/// `break` inside a `try` block, which the host runs as its own chunk); a
746/// same-chunk `break` is a plain compiler-resolved jump.
747#[derive(Clone)]
748pub enum Signal {
749    Return(Value),
750    Break(Option<String>),
751    Continue(Option<String>),
752}
753
754/// The JavaScript runtime.
755pub struct JsHost {
756    heap: Vec<JsObj>,
757    /// Function templates, indexed by def id.
758    pub funcs: Vec<FuncDef>,
759    /// try/catch/finally block templates, indexed by try id.
760    pub tries: Vec<TryDef>,
761    /// Module-level (global) names.
762    globals: VarMap,
763    /// Top-level `const` names (a module frame declares into `globals`), so an
764    /// assignment to one throws the same way a block-scoped `const` does.
765    global_consts: rustc_hash::FxHashSet<String>,
766    /// The frame stack (bottom = module).
767    frames: Vec<Frame>,
768    /// The program's top-level scope — the scope runtime-compiled source runs in
769    /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
770    /// `run_chunk_in_global_scope`), as opposed to whatever function frame
771    /// happens to be executing when that source is compiled.
772    ///
773    /// Held as its own field rather than read off `frames[0]` because a coroutine
774    /// body runs with `frames` SWAPPED for its own one-frame context
775    /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
776    ///
777    /// Note this is node-js's ONE top-level scope. Node distinguishes the global
778    /// scope from a CommonJS module's scope (a module body is a wrapper
779    /// function), so in Node a file's top-level `var` is invisible to dynamic
780    /// code; here the entry file is evaluated with Script semantics, so it stays
781    /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
782    /// records for top-level `return`, not a separate one — and `node -e`, which
783    /// really is a Script, matches Node exactly.
784    global_env: Env,
785    pub error: Option<String>,
786    /// The in-flight thrown value, if any (JS `throw`).
787    pub exc: Option<Value>,
788    pub signal: Option<Signal>,
789    /// Promises that settled REJECTED this tick. Drained at each microtask
790    /// checkpoint: any still without a handler is an unhandled rejection.
791    pub pending_rejections: Vec<u32>,
792    /// `process.on(event, fn)` listeners, by event name.
793    pub process_listeners: IndexMap<String, Vec<ProcListener>>,
794    /// The canonical `null` handle (allocated once).
795    null_val: Value,
796    /// `[[Prototype]]` link per heap object, by heap index. Absent = default
797    /// (`Object.prototype` for objects, `null` for the root).
798    protos: HashMap<u32, Value>,
799    /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
800    /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
801    /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
802    /// is why `Object.create(null) instanceof Object` can read `false`.
803    null_proto_objs: HashSet<u32>,
804    /// Own properties of function objects (functions are objects in JS): a live
805    /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
806    fn_props: HashMap<u32, IndexMap<String, Value>>,
807    /// Accessor (getter/setter) properties per owning object, by heap index then
808    /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
809    accessors: HashMap<u32, IndexMap<String, Accessor>>,
810    /// Own-property attributes that deviate from the plain-assignment default
811    /// (`{writable, enumerable, configurable}` all true), by heap index then key.
812    /// Only non-default entries are stored, so an ordinary object costs nothing;
813    /// `prop_attrs` returns the default for any key absent here. This is what
814    /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
815    /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
816    /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
817    prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
818    /// Heap objects sealed against new properties by `Object.preventExtensions`,
819    /// `Object.seal` or `Object.freeze`.
820    non_extensible: HashSet<u32>,
821    /// Private names (`#m`) declared as a METHOD or accessor rather than as a
822    /// field, for the brand-check error text: node distinguishes `Receiver must
823    /// be an instance of class C` (a private method or accessor) from `Cannot
824    /// read private member #x …` (a private field). Which class is answered by
825    /// the running method's home class, not by this set, so two classes
826    /// declaring the same private method name stay exact.
827    private_methods: HashSet<String>,
828    /// The ELIDED element positions of each array, by heap index. Absent (the
829    /// overwhelmingly common case) means the array is dense.
830    ///
831    /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
832    /// to be mapped back to `undefined` at every element read in the runtime, and
833    /// a single missed read would leak an un-nameable value into user code — a
834    /// worse failure than storing `undefined` and losing the distinction. Keeping
835    /// the marker OUTSIDE the value domain makes that leak structurally
836    /// impossible: the element vector still holds a perfectly ordinary
837    /// `Value::Undef` at a hole, so any code path that has not been taught about
838    /// holes degrades to exactly the pre-existing behaviour (a visible
839    /// `undefined`) instead of producing something unrepresentable.
840    ///
841    /// Sized like the array it describes in the worst case (`new Array(n)` marks
842    /// every index), which is the same order as the `Vec<Value>` already paid for
843    /// that array — so it cannot turn a working allocation into an OOM.
844    array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
845    /// User-assigned static properties on a builtin namespace/constructor, keyed
846    /// by namespace name then property (`Error` → `prepareStackTrace`,
847    /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
848    /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
849    /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
850    builtin_statics: HashMap<String, IndexMap<String, Value>>,
851    /// The shared well-known `Object.prototype` object (chain root for objects).
852    object_proto: Value,
853    /// Class name of each class `prototype` object, by heap index — lets an
854    /// instance recover its constructor name (for `util.inspect` prefix and
855    /// `obj.constructor.name`).
856    proto_class: HashMap<u32, Value>,
857    /// Class constructor values by name, so a running method's `home_class` name
858    /// resolves to its class value (for `super`).
859    class_registry: HashMap<String, Value>,
860    /// Well-known prototype objects for the builtin error constructors, by name.
861    error_protos: HashMap<String, Value>,
862    /// Real prototype *objects* for the builtin exotics whose instances need a
863    /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
864    /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
865    /// cannot appear on a prototype chain and report `typeof "function"`.
866    native_protos: HashMap<String, Value>,
867    /// `Symbol.for` registry: description → symbol value.
868    symbol_registry: HashMap<String, Value>,
869    /// Monotonic id source for fresh `Symbol()` values.
870    next_symbol: u64,
871    /// Every live symbol by its id, so a `@@sym:<id>` property key can be
872    /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
873    symbols_by_id: HashMap<u64, Value>,
874    /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
875    /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
876    /// is a distinct key.
877    well_known_ids: HashMap<u64, String>,
878    /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
879    generators: Vec<GenCell>,
880    /// Promise cells, indexed by `JsObj::Promise.id`.
881    promises: Vec<PromiseCell>,
882    /// `process.nextTick` callbacks (drained before promise microtasks).
883    pub nextticks: std::collections::VecDeque<Task>,
884    /// Promise-reaction / `queueMicrotask` microtasks.
885    pub microtasks: std::collections::VecDeque<Task>,
886    /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
887    pub macrotasks: Vec<Timer>,
888    /// Monotonic timer-id source.
889    next_timer: u64,
890    /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
891    /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
892    /// sees a spurious `Disconnected` while a server is running.
893    io_tx: Sender<IoTask>,
894    /// Owned by the event loop (taken out for the blocking `recv`). Receives the
895    /// `IoTask`s posted by I/O threads.
896    io_rx: Option<Receiver<IoTask>>,
897    /// Ref-count of "things keeping the process alive": open listeners, live
898    /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
899    /// queues are empty. A pure script never touches it, so it exits exactly as
900    /// before.
901    open_handles: usize,
902    /// In-process output sink. When `Some`, everything the program writes to
903    /// stdout/stderr is appended here instead of reaching the process streams —
904    /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
905    /// cannot corrupt its display. `None` (the default) is the ordinary
906    /// standalone `node` behaviour: writes go straight to the real streams.
907    ///
908    /// Bytes, not `String`: a program may legitimately write output that is not
909    /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
910    /// buffer can only hold the lossy `U+FFFD` transcription of it.
911    capture: Option<Vec<u8>>,
912    /// `process.exitCode`: the code the process exits with when the event loop
913    /// drains, or `None` while unset. Separate from an explicit
914    /// `process.exit(n)`, which exits immediately with `n`.
915    pub exit_code: Option<i32>,
916    /// Whether the `exit` event has already been emitted, so the `process.exit`
917    /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
918    pub exiting: bool,
919    /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
920    /// identity in JS, so `globalThis === globalThis` is `true` and a property
921    /// written through one read is visible through the next. Minting a fresh
922    /// object per read made both false.
923    global_obj: Value,
924}
925
926/// One `process.on`/`process.once` registration. `once` is not decoration: a
927/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
928/// the same event does not reach it. Treating `once` as an alias of `on` made
929/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
930/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
931/// reports zero listeners afterwards.
932#[derive(Clone)]
933pub struct ProcListener {
934    pub f: Value,
935    pub once: bool,
936}
937
938/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
939/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
940pub enum Task {
941    Js { cb: Value, args: Vec<Value> },
942    Native(Box<dyn FnOnce() -> Result<(), String>>),
943}
944
945impl Task {
946    fn run(self) -> Result<(), String> {
947        match self {
948            Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
949            Task::Native(f) => f(),
950        }
951    }
952}
953
954/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
955/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
956pub struct Timer {
957    pub id: u64,
958    pub delay: f64,
959    pub seq: u64,
960    pub callback: Value,
961    pub args: Vec<Value>,
962    pub cancelled: bool,
963    /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
964    /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
965    /// deadline each time it fires, so it keeps the loop alive indefinitely —
966    /// exactly like Node, where `setInterval` runs until cleared.
967    pub interval: Option<f64>,
968    /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
969    /// the event loop alive; an unref'd one still fires while the loop happens
970    /// to be alive for another reason, but never holds it open by itself.
971    pub refed: bool,
972    /// Real wall-clock deadline (`now + delay`), used only on the real-clock
973    /// path (an open handle or a pending interval). On the pure virtual clock
974    /// this is ignored.
975    pub deadline: Instant,
976}
977
978/// One suspended generator. `coro` is `None` only while actively running (taken
979/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
980/// (frames/signal/error/exc) while suspended.
981struct GenCell {
982    coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
983    /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
984    /// thread → valid for the body's life). Read by `yield` to suspend.
985    yielder: *const (),
986    ctx: GenContext,
987    done: bool,
988    /// True once the body has been resumed at least once (so it is suspended at a
989    /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
990    started: bool,
991    /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
992    /// `yield` resume so the body unwinds (running any pending `finally`).
993    inject: Option<GenInject>,
994    /// True for an `async function*` body, where `await` AND `yield` share one
995    /// coroutine yielder: `await` wraps its operand in an await marker so the
996    /// driver can tell an internal suspension from a real yield.
997    async_gen: bool,
998    /// `[[AsyncGeneratorQueue]]` — pending requests as
999    /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1000    /// overlapping requests resume the body ONE AT A TIME and settle in request
1001    /// order; without it a second request issued before the first settles races
1002    /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1003    /// all enqueue — a `.return()` that skipped the queue would terminate the
1004    /// body while an earlier `.next()` was still suspended on an `await`, and
1005    /// that `.next()` would then wrongly report `{done: true}`.
1006    queue: std::collections::VecDeque<(GenReq, u32)>,
1007    /// True while a queued request is being driven.
1008    running: bool,
1009    /// The [`stack_floor`] that applies while this generator's body is running.
1010    ///
1011    /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1012    /// range the thread's pthread record describes says nothing about how much
1013    /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1014    /// construction and swapped in around every resume; without it the guard
1015    /// compared a coroutine stack pointer against the main stack's floor and
1016    /// (depending on where mmap landed) either fired immediately or never.
1017    stack_floor: usize,
1018}
1019
1020/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1021enum GenInject {
1022    Return(Value),
1023    Throw(Value),
1024}
1025
1026/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1027/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1028/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1029/// instead of unwinding the body on the spot.
1030#[derive(Clone)]
1031pub enum GenReq {
1032    /// `.next(v)` — resume normally with `v`.
1033    Next(Value),
1034    /// `.return(v)` — resume with a forced return completion.
1035    Return(Value),
1036    /// `.throw(e)` — resume with a forced throw completion.
1037    Throw(Value),
1038}
1039
1040/// The mutable "execution registers" swapped at every generator resume/suspend
1041/// boundary so a suspended generator's half-finished frame/signal state never
1042/// leaks into the resuming caller. The heap, function/class tables and globals
1043/// are shared and never swapped.
1044#[derive(Default)]
1045struct GenContext {
1046    frames: Vec<Frame>,
1047    error: Option<String>,
1048    exc: Option<Value>,
1049    signal: Option<Signal>,
1050}
1051
1052thread_local! {
1053    /// Id of the generator whose body is currently executing, or `None` at the
1054    /// root. `yield` suspends this generator.
1055    static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1056}
1057
1058thread_local! {
1059    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1060}
1061
1062/// Run `f` with mutable access to the thread-local host.
1063pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1064    HOST.with(|h| f(&mut h.borrow_mut()))
1065}
1066
1067/// Reset the host to a clean slate (fresh module frame).
1068pub fn reset_host() {
1069    with_host(|h| *h = JsHost::new());
1070    // Drop any cached module handles / factory closure — they index the old heap.
1071    crate::module::reset();
1072}
1073
1074impl Default for JsHost {
1075    fn default() -> Self {
1076        Self::new()
1077    }
1078}
1079
1080impl JsHost {
1081    pub fn new() -> JsHost {
1082        let global_env = new_env(None);
1083        let (io_tx, io_rx) = std::sync::mpsc::channel();
1084        let mut h = JsHost {
1085            heap: Vec::new(),
1086            funcs: Vec::new(),
1087            tries: Vec::new(),
1088            globals: VarMap::default(),
1089            global_consts: rustc_hash::FxHashSet::default(),
1090            frames: vec![Frame {
1091                env: global_env.clone(),
1092                base_env: global_env.clone(),
1093                this_obj: None,
1094                new_target: None,
1095                home_class: None,
1096                line: 0,
1097                owner: None,
1098                is_module: true,
1099            }],
1100            global_env,
1101            error: None,
1102            exc: None,
1103            signal: None,
1104            pending_rejections: Vec::new(),
1105            process_listeners: IndexMap::new(),
1106            null_val: Value::Undef,
1107            protos: HashMap::new(),
1108            null_proto_objs: HashSet::new(),
1109            fn_props: HashMap::new(),
1110            accessors: HashMap::new(),
1111            prop_attrs: HashMap::new(),
1112            non_extensible: HashSet::new(),
1113            private_methods: HashSet::new(),
1114            array_holes: HashMap::new(),
1115            builtin_statics: HashMap::new(),
1116            object_proto: Value::Undef,
1117            proto_class: HashMap::new(),
1118            class_registry: HashMap::new(),
1119            error_protos: HashMap::new(),
1120            native_protos: HashMap::new(),
1121            symbol_registry: HashMap::new(),
1122            next_symbol: 1,
1123            symbols_by_id: HashMap::new(),
1124            well_known_ids: HashMap::new(),
1125            generators: Vec::new(),
1126            promises: Vec::new(),
1127            microtasks: std::collections::VecDeque::new(),
1128            nextticks: std::collections::VecDeque::new(),
1129            macrotasks: Vec::new(),
1130            next_timer: 1,
1131            io_tx,
1132            io_rx: Some(io_rx),
1133            open_handles: 0,
1134            capture: None,
1135            exit_code: None,
1136            exiting: false,
1137            global_obj: Value::Undef,
1138        };
1139        h.null_val = h.alloc(JsObj::Null);
1140        // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1141        h.object_proto = h.new_object(IndexMap::new());
1142        h.global_obj = h.new_object(IndexMap::new());
1143        h
1144    }
1145
1146    /// Whether `v` IS the one `globalThis` object (not merely an object).
1147    pub fn is_global_object(&self, v: &Value) -> bool {
1148        !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1149    }
1150
1151    /// The `globalThis` object — one per host, so its identity and its
1152    /// properties both survive across reads.
1153    pub fn global_object(&mut self) -> Value {
1154        if matches!(self.global_obj, Value::Undef) {
1155            self.global_obj = self.new_object(IndexMap::new());
1156        }
1157        self.global_obj.clone()
1158    }
1159
1160    // ── prototype chain ──────────────────────────────────────────────────
1161    /// The `[[Prototype]]` of a heap value, if explicitly linked.
1162    pub fn proto_of(&self, v: &Value) -> Option<Value> {
1163        if let Value::Obj(i) = v {
1164            self.protos.get(i).cloned()
1165        } else {
1166            None
1167        }
1168    }
1169    /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1170    /// null-prototype object (recorded so `instanceof Object` reads false);
1171    /// undefined just clears any link without the null marker.
1172    pub fn set_proto(&mut self, v: &Value, proto: Value) {
1173        if let Value::Obj(i) = v {
1174            if self.is_null(&proto) {
1175                self.protos.remove(i);
1176                self.null_proto_objs.insert(*i);
1177            } else if matches!(proto, Value::Undef) {
1178                self.protos.remove(i);
1179            } else {
1180                self.protos.insert(*i, proto);
1181                self.null_proto_objs.remove(i);
1182            }
1183        }
1184    }
1185    /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1186    pub fn has_null_proto(&self, v: &Value) -> bool {
1187        matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1188    }
1189    pub fn object_proto(&self) -> Value {
1190        self.object_proto.clone()
1191    }
1192    /// Record that the prototype object `proto` belongs to the class constructor
1193    /// `class_val` (so instances can recover their constructor).
1194    pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1195        if let Value::Obj(i) = proto {
1196            self.proto_class.insert(*i, class_val);
1197        }
1198    }
1199    /// The class constructor value nearest in `obj`'s prototype chain, if any.
1200    pub fn class_of(&self, obj: &Value) -> Option<Value> {
1201        let mut cur = self.proto_of(obj);
1202        while let Some(p) = cur {
1203            if let Value::Obj(i) = &p {
1204                if let Some(c) = self.proto_class.get(i) {
1205                    return Some(c.clone());
1206                }
1207            }
1208            cur = self.proto_of(&p);
1209        }
1210        None
1211    }
1212    /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1213    /// object, no prefix).
1214    pub fn ctor_name(&self, obj: &Value) -> String {
1215        if let Some(c) = self.class_of(obj) {
1216            if let Some(JsObj::Class(cv)) = self.get(&c) {
1217                return cv.name.clone();
1218            }
1219        }
1220        // A `function F(){}` constructor is not a `class`, so it has no
1221        // `proto_class` entry. V8's `getConstructorName` walks the prototype
1222        // chain for an own `constructor` that is a named function — which is
1223        // what makes `console.log(new F())` print `F { y: 2 }`.
1224        let mut cur = self.proto_of(obj);
1225        while let Some(p) = cur {
1226            let ctor = match self.get(&p) {
1227                Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1228                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1229                _ => None,
1230            };
1231            if let Some(f) = ctor {
1232                let n = self.callable_name(&f);
1233                if !n.is_empty() {
1234                    return n;
1235                }
1236            }
1237            cur = self.proto_of(&p);
1238        }
1239        String::new()
1240    }
1241
1242    /// Whether a callable owns a `prototype` property. `MakeConstructor`
1243    /// (10.2.5) runs for an ordinary function definition and for every
1244    /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1245    /// function are not constructors and own none.
1246    pub fn owns_prototype(&self, v: &Value) -> bool {
1247        match self.get(v) {
1248            Some(JsObj::Class(_)) => true,
1249            Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1250                Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1251                None => false,
1252            },
1253            _ => false,
1254        }
1255    }
1256
1257    /// A function's own-property table (created on demand).
1258    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1259        if let Value::Obj(i) = v {
1260            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1261        } else {
1262            None
1263        }
1264    }
1265
1266    /// A class static member, inherited down the constructor chain: a subclass
1267    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1268    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1269        let mut cur = class_val.clone();
1270        loop {
1271            if let Some(v) = self.fn_prop(&cur, name) {
1272                return Some(v);
1273            }
1274            match self.get(&cur) {
1275                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1276                _ => return None,
1277            }
1278        }
1279    }
1280
1281    /// The first `extends` ancestor that is NOT a user class — the builtin
1282    /// constructor a class chain bottoms out in (`class D extends Array {}` →
1283    /// the `Array` builtin), or `None` for a chain of user classes only.
1284    ///
1285    /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1286    /// stops being a `Class`, so a static declared by the BUILTIN half of the
1287    /// chain was unreachable: `D.from` read `undefined` where node inherits
1288    /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1289    /// with an ordinary property read, which is what reaches a builtin's
1290    /// statics.
1291    pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1292        let mut cur = class_val.clone();
1293        loop {
1294            match self.get(&cur) {
1295                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1296                _ => return Some(cur),
1297            }
1298        }
1299    }
1300    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1301        if let Value::Obj(i) = v {
1302            self.fn_props
1303                .entry(*i)
1304                .or_default()
1305                .insert(name.to_string(), val);
1306        }
1307        // `name` and `prototype` are own properties of every function/class, but
1308        // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1309        // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1310        // script assigned. An ARRAY receiver reaching the same side table has no
1311        // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1312        if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1313            return;
1314        }
1315        let attrs = match name {
1316            "name" => PropAttrs {
1317                writable: false,
1318                enumerable: false,
1319                configurable: true,
1320            },
1321            "prototype" => PropAttrs {
1322                writable: true,
1323                enumerable: false,
1324                configurable: false,
1325            },
1326            _ => return,
1327        };
1328        self.set_prop_attrs(v, name, attrs);
1329    }
1330    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1331    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1332        self.builtin_statics
1333            .get(ns)
1334            .and_then(|m| m.get(name).cloned())
1335    }
1336    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1337    /// handles for the same namespace).
1338    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1339        self.builtin_statics
1340            .entry(ns.to_string())
1341            .or_default()
1342            .insert(name.to_string(), val);
1343    }
1344    /// Drop an own property from the side table (`delete arr.foo`,
1345    /// `delete fn.tag`). Reports whether the key was there.
1346    pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1347        match v {
1348            Value::Obj(i) => self
1349                .fn_props
1350                .get_mut(i)
1351                .map(|m| m.shift_remove(name).is_some())
1352                .unwrap_or(false),
1353            _ => false,
1354        }
1355    }
1356    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1357        if let Value::Obj(i) = v {
1358            self.fn_props
1359                .get(i)
1360                .map(|m| m.keys().cloned().collect())
1361                .unwrap_or_default()
1362        } else {
1363            Vec::new()
1364        }
1365    }
1366
1367    /// Install an accessor `(get, set)` for `key` on the object `owner`.
1368    pub fn set_accessor(
1369        &mut self,
1370        owner: &Value,
1371        key: &str,
1372        get: Option<Value>,
1373        set: Option<Value>,
1374    ) {
1375        if let Value::Obj(i) = owner {
1376            // Accessors live in their own table, but JS reports own keys in a
1377            // single insertion order across data AND accessor properties. Drop an
1378            // ordering marker into the property map so
1379            // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1380            // The marker is `@@`-prefixed, so it is invisible to every reader.
1381            let marker = format!("{ORD_MARKER}{key}");
1382            match self.get_mut(owner) {
1383                Some(JsObj::Object(props)) => {
1384                    if !props.contains_key(key) && !props.contains_key(&marker) {
1385                        props.insert(marker, Value::Undef);
1386                    }
1387                }
1388                // A function or class keeps its own properties in the fn-prop
1389                // side table, so its ordering marker belongs there. Without it a
1390                // static accessor enumerated AFTER every static field and method
1391                // regardless of where the class body declared it: node reports
1392                // `class A { static s = 2; static get sv(){} static m(){} }` as
1393                // `['sv', 'm', 's']` — the methods and accessors in source order
1394                // first, then the fields — and this reported `['m', 's', 'sv']`.
1395                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1396                    let table = self.fn_props.entry(*i).or_default();
1397                    if !table.contains_key(key) && !table.contains_key(&marker) {
1398                        table.insert(marker, Value::Undef);
1399                    }
1400                }
1401                _ => {}
1402            }
1403            let slot = self
1404                .accessors
1405                .entry(*i)
1406                .or_default()
1407                .entry(key.to_string())
1408                .or_insert((None, None));
1409            if get.is_some() {
1410                slot.0 = get;
1411            }
1412            if set.is_some() {
1413                slot.1 = set;
1414            }
1415        }
1416    }
1417    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1418    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1419        if let Value::Obj(i) = owner {
1420            self.accessors.get(i).and_then(|m| m.get(key).cloned())
1421        } else {
1422            None
1423        }
1424    }
1425
1426    /// The own accessor-property keys of `owner`, in installation order.
1427    pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1428        match owner {
1429            Value::Obj(i) => self
1430                .accessors
1431                .get(i)
1432                .map(|m| m.keys().cloned().collect())
1433                .unwrap_or_default(),
1434            _ => Vec::new(),
1435        }
1436    }
1437
1438    // ── own-property attributes ──────────────────────────────────────────
1439
1440    /// Record non-default attributes for `owner[key]`. Storing the default shape
1441    /// clears the entry so the table only ever holds deviations.
1442    pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1443        if let Value::Obj(i) = owner {
1444            if attrs == PropAttrs::default() {
1445                if let Some(m) = self.prop_attrs.get_mut(i) {
1446                    m.shift_remove(key);
1447                }
1448            } else {
1449                self.prop_attrs
1450                    .entry(*i)
1451                    .or_default()
1452                    .insert(key.to_string(), attrs);
1453            }
1454        }
1455    }
1456
1457    /// Copy every recorded property attribute from `from` to `to`. A pass that
1458    /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1459    /// across or the copy silently re-exposes non-enumerable slots.
1460    pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1461        if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1462            if let Some(m) = self.prop_attrs.get(f).cloned() {
1463                for (k, a) in m {
1464                    self.set_prop_attrs(to, &k, a);
1465                }
1466            }
1467        }
1468    }
1469
1470    /// The attributes of own property `owner[key]` (all-true when unrecorded).
1471    pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1472        // An array's `length` is the array exotic's own property (10.4.2):
1473        // writable, but never enumerated and never configurable.
1474        if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1475            return PropAttrs {
1476                writable: true,
1477                enumerable: false,
1478                configurable: false,
1479            };
1480        }
1481        match owner {
1482            Value::Obj(i) => self
1483                .prop_attrs
1484                .get(i)
1485                .and_then(|m| m.get(key))
1486                .copied()
1487                .unwrap_or_default(),
1488            _ => PropAttrs::default(),
1489        }
1490    }
1491
1492    /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1493    /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1494    pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1495        !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1496    }
1497
1498    /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1499    /// shape of every V8 "hidden but real" own property.
1500    pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1501        self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1502    }
1503
1504    /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1505    /// non-writable data property silently ignores the write in sloppy mode,
1506    /// which is the mode every script here runs in; so does adding a *new* key to
1507    /// a non-extensible object.
1508    pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1509        if !self.prop_attrs(owner, key).writable {
1510            return false;
1511        }
1512        if self.is_extensible(owner) {
1513            return true;
1514        }
1515        match self.get(owner) {
1516            Some(JsObj::Object(p)) => p.contains_key(key),
1517            _ => true,
1518        }
1519    }
1520
1521    /// Mark `v` closed to new properties (`Object.preventExtensions`).
1522    pub fn prevent_extensions(&mut self, v: &Value) {
1523        if let Value::Obj(i) = v {
1524            self.non_extensible.insert(*i);
1525        }
1526    }
1527
1528    pub fn is_extensible(&self, v: &Value) -> bool {
1529        !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1530    }
1531
1532    /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1533    /// the object and strip `configurable` — and, when freezing, `writable` —
1534    /// from every own property, data and accessor alike.
1535    pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1536        self.prevent_extensions(v);
1537        let mut keys = match self.get(v) {
1538            Some(JsObj::Object(p)) => p.keys().cloned().collect::<Vec<_>>(),
1539            _ => Vec::new(),
1540        };
1541        keys.extend(self.own_accessor_keys(v));
1542        for k in keys {
1543            let mut a = self.prop_attrs(v, &k);
1544            a.configurable = false;
1545            if freeze {
1546                a.writable = false;
1547            }
1548            self.set_prop_attrs(v, &k, a);
1549        }
1550    }
1551
1552    /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1553    pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1554        if self.is_extensible(v) {
1555            return false;
1556        }
1557        let mut keys = match self.get(v) {
1558            Some(JsObj::Object(p)) => p.keys().cloned().collect::<Vec<_>>(),
1559            _ => Vec::new(),
1560        };
1561        keys.extend(self.own_accessor_keys(v));
1562        keys.iter().all(|k| {
1563            let a = self.prop_attrs(v, k);
1564            !a.configurable && (!freeze || !a.writable)
1565        })
1566    }
1567
1568    /// A fresh unique `Symbol(desc)` value.
1569    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1570        let id = self.next_symbol;
1571        self.next_symbol += 1;
1572        let v = self.alloc(JsObj::Symbol { desc, id });
1573        self.symbols_by_id.insert(id, v.clone());
1574        v
1575    }
1576
1577    /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1578    /// well-known `@@iterator`) came from.
1579    pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1580        if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1581            return self.symbols_by_id.get(&id).cloned();
1582        }
1583        let name = k.strip_prefix("@@")?;
1584        WELL_KNOWN_SYMBOLS
1585            .contains(&name)
1586            .then(|| {
1587                self.symbol_registry
1588                    .get(&format!("@@Symbol.{name}"))
1589                    .cloned()
1590            })
1591            .flatten()
1592    }
1593
1594    /// The own symbol-keyed property keys of `v` as SYMBOL values —
1595    /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
1596    pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
1597        let keys: Vec<String> = match self.get(v) {
1598            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1599            // An Array/Function receiver has no property map: its non-index own
1600            // properties — symbol-keyed ones included — live in the fn-prop side
1601            // table, and are just as much own properties as an object's.
1602            Some(_) => self.fn_prop_keys(v),
1603            None => return Vec::new(),
1604        };
1605        keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
1606    }
1607
1608    /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
1609    /// what `CopyDataProperties` (object spread, `Object.assign`) copies
1610    /// alongside the string keys, and what `Object.keys` / `for-in` /
1611    /// `JSON.stringify` deliberately skip.
1612    pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
1613        match self.get(v) {
1614            Some(JsObj::Object(p)) => p
1615                .iter()
1616                .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
1617                .map(|(k, val)| (k.clone(), val.clone()))
1618                .collect(),
1619            // Array/Function: the side table (see `own_symbol_keys`).
1620            Some(_) => self
1621                .fn_prop_keys(v)
1622                .into_iter()
1623                .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
1624                .map(|k| {
1625                    let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
1626                    (k, val)
1627                })
1628                .collect(),
1629            None => Vec::new(),
1630        }
1631    }
1632    /// The shared `Symbol.for(key)` value (interned by description).
1633    pub fn symbol_for(&mut self, key: &str) -> Value {
1634        if let Some(v) = self.symbol_registry.get(key) {
1635            return v.clone();
1636        }
1637        let s = self.new_symbol(Some(key.to_string()));
1638        self.symbol_registry.insert(key.to_string(), s.clone());
1639        s
1640    }
1641    /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
1642    /// or `undefined` for a symbol that is not in the registry at all.
1643    ///
1644    /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
1645    /// `Symbol('k')` share a description and only the first is registered. The
1646    /// `@@Symbol.*` well-known entries are registry-internal and never a
1647    /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
1648    /// `undefined` there.
1649    pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
1650        let Some(key) = self
1651            .symbol_registry
1652            .iter()
1653            .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
1654            .map(|(k, _)| k.clone())
1655        else {
1656            return Value::Undef;
1657        };
1658        self.new_str(key)
1659    }
1660    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
1661    /// property key is `@@iterator`).
1662    pub fn well_known_iterator(&mut self) -> Value {
1663        self.symbol_for("@@Symbol.iterator")
1664    }
1665    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
1666    pub fn well_known_async_iterator(&mut self) -> Value {
1667        self.symbol_for("@@Symbol.asyncIterator")
1668    }
1669    /// A well-known symbol by its ECMAScript name (`toPrimitive`,
1670    /// `toStringTag`, …). Its internal property key is `@@<name>` — see
1671    /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
1672    ///
1673    /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
1674    /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
1675    /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
1676    /// different symbol, and identification is by id, so a user-made
1677    /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
1678    pub fn well_known_symbol(&mut self, name: &str) -> Value {
1679        let key = format!("@@Symbol.{name}");
1680        if let Some(v) = self.symbol_registry.get(&key) {
1681            return v.clone();
1682        }
1683        let s = self.new_symbol(Some(format!("Symbol.{name}")));
1684        if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
1685            self.well_known_ids.insert(*id, name.to_string());
1686        }
1687        self.symbol_registry.insert(key, s.clone());
1688        s
1689    }
1690    /// The internal property-key string for a value used as a key. A `Symbol`
1691    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
1692    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
1693    pub fn property_key(&self, v: &Value) -> String {
1694        if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
1695            if let Some(n) = self.well_known_ids.get(id) {
1696                return format!("@@{n}");
1697            }
1698            return format!("@@sym:{id}");
1699        }
1700        self.str_of(v)
1701    }
1702
1703    pub fn null(&self) -> Value {
1704        self.null_val.clone()
1705    }
1706    pub fn is_null(&self, v: &Value) -> bool {
1707        matches!(self.get(v), Some(JsObj::Null))
1708    }
1709
1710    // ── program loading ──────────────────────────────────────────────────
1711    pub fn program_offsets(&self) -> (usize, usize) {
1712        (self.funcs.len(), self.tries.len())
1713    }
1714    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
1715        self.funcs.extend(funcs);
1716        self.tries.extend(tries);
1717    }
1718    pub fn try_def(&self, id: usize) -> Option<TryDef> {
1719        self.tries.get(id).cloned()
1720    }
1721
1722    /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
1723    /// finalizer)` — without copying its chunks. Running a `try` used to clone
1724    /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
1725    /// handler and its finalizer on every iteration just to learn its shape.
1726    pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
1727        let t = self.tries.get(id)?;
1728        Some((
1729            t.handler.is_some(),
1730            t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
1731            t.finalizer.is_some(),
1732        ))
1733    }
1734
1735    /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
1736    /// Reached only when no pooled VM already holds that chunk.
1737    pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
1738        let t = self.tries.get(id)?;
1739        match part {
1740            0 => Some(t.block.clone()),
1741            1 => t.handler.as_ref().map(|(_, body)| body.clone()),
1742            _ => t.finalizer.clone(),
1743        }
1744    }
1745
1746    // ── heap allocation / accessors ──────────────────────────────────────
1747    pub fn alloc(&mut self, obj: JsObj) -> Value {
1748        self.heap.push(obj);
1749        Value::Obj((self.heap.len() - 1) as u32)
1750    }
1751    pub fn get(&self, v: &Value) -> Option<&JsObj> {
1752        if let Value::Obj(i) = v {
1753            self.heap.get(*i as usize)
1754        } else {
1755            None
1756        }
1757    }
1758    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
1759        if let Value::Obj(i) = v {
1760            self.heap.get_mut(*i as usize)
1761        } else {
1762            None
1763        }
1764    }
1765    /// Which variant `v` points at, without copying its contents. Use this in
1766    /// place of `get(v).cloned()` whenever only the tag is needed — see
1767    /// [`ObjKind`].
1768    pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
1769        self.get(v).map(JsObj::kind)
1770    }
1771    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
1772        self.alloc(JsObj::Str(s.into()))
1773    }
1774    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
1775        self.alloc(JsObj::Array(items))
1776    }
1777
1778    /// Record that `name` was declared as a private method or accessor.
1779    pub fn note_private_method(&mut self, name: &str) {
1780        self.private_methods.insert(name.to_string());
1781    }
1782
1783    /// Whether `name` was declared as a private method/accessor by some class,
1784    /// as opposed to a private field.
1785    pub fn is_private_method(&self, name: &str) -> bool {
1786        self.private_methods.contains(name)
1787    }
1788
1789    /// The name of the class whose body the running function belongs to. Only a
1790    /// method of that class can even mention its private names, so this is the
1791    /// class a failed brand check must name.
1792    pub fn current_home_class_name(&self) -> Option<String> {
1793        match self.get(&self.current_home_class()?) {
1794            Some(JsObj::Class(c)) => Some(c.name.clone()),
1795            _ => None,
1796        }
1797    }
1798
1799    /// Whether `recv` — or anything on its prototype chain — carries the private
1800    /// name `key`. A private FIELD is an own property of the instance; a private
1801    /// METHOD lives on the class prototype, one link up.
1802    pub fn has_private(&self, recv: &Value, key: &str) -> bool {
1803        let mut cur = Some(recv.clone());
1804        while let Some(v) = cur {
1805            let owns = match self.get(&v) {
1806                Some(JsObj::Object(p)) => p.contains_key(key),
1807                Some(JsObj::Class(c)) => c.statics.contains_key(key),
1808                _ => false,
1809            };
1810            if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
1811                return true;
1812            }
1813            cur = self.proto_of(&v);
1814        }
1815        false
1816    }
1817
1818    // ── array holes ──────────────────────────────────────────────────────
1819    //
1820    // Every read/write of an array's elision set goes through this block. See
1821    // the `array_holes` field for why the marker lives here rather than in
1822    // `Value`.
1823
1824    /// Whether element `i` of array `arr` is an elided element (a "hole"), as
1825    /// opposed to a stored `undefined`. `false` for anything that is not an
1826    /// array, and for every index of a dense one.
1827    pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
1828        match (arr, ()) {
1829            (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
1830            _ => false,
1831        }
1832    }
1833
1834    /// Whether `arr` has any elided element at all — one hash probe, and the
1835    /// guard every hole-aware code path takes before doing anything slower.
1836    pub fn has_holes(&self, arr: &Value) -> bool {
1837        matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
1838    }
1839
1840    /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
1841    /// Sorted because every consumer (own-key enumeration, `util.inspect`
1842    /// run-grouping) needs index order, and the backing set has none.
1843    pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
1844        let Value::Obj(i) = arr else {
1845            return Vec::new();
1846        };
1847        let Some(hs) = self.array_holes.get(i) else {
1848            return Vec::new();
1849        };
1850        let mut v: Vec<usize> = hs.iter().copied().collect();
1851        v.sort_unstable();
1852        v
1853    }
1854
1855    /// Record element `i` of `arr` as elided.
1856    pub fn mark_hole(&mut self, arr: &Value, i: usize) {
1857        if let Value::Obj(idx) = arr {
1858            self.array_holes.entry(*idx).or_default().insert(i);
1859        }
1860    }
1861
1862    /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
1863    /// the gap a write past the end opens).
1864    pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
1865        if range.is_empty() {
1866            return;
1867        }
1868        if let Value::Obj(idx) = arr {
1869            self.array_holes.entry(*idx).or_default().extend(range);
1870        }
1871    }
1872
1873    /// Element `i` now holds a real value: it is no longer a hole. Every write
1874    /// to an array index calls this, which is what keeps a stale hole record
1875    /// from outliving the elision it described.
1876    pub fn clear_hole(&mut self, arr: &Value, i: usize) {
1877        let Value::Obj(idx) = arr else { return };
1878        let Some(hs) = self.array_holes.get_mut(idx) else {
1879            return;
1880        };
1881        hs.remove(&i);
1882        if hs.is_empty() {
1883            self.array_holes.remove(idx);
1884        }
1885    }
1886
1887    /// `arr` is dense from here on (`fill` over the whole array, a fresh
1888    /// dense assignment into an existing handle).
1889    pub fn clear_holes(&mut self, arr: &Value) {
1890        if let Value::Obj(idx) = arr {
1891            self.array_holes.remove(idx);
1892        }
1893    }
1894
1895    /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
1896    /// `f`. Used by every method that derives a new array whose holes track the
1897    /// source's (`slice`, `concat`, `map`).
1898    pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
1899        if !self.has_holes(src) {
1900            return;
1901        }
1902        let moved: rustc_hash::FxHashSet<usize> =
1903            self.hole_indices(src).into_iter().filter_map(f).collect();
1904        self.install_holes(dst, moved);
1905    }
1906
1907    /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
1908    /// existing hole moves to, or `None` if the mutation removed it. This is the
1909    /// one primitive behind every structural array mutation — `shift` is
1910    /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
1911    /// `splice` is the general case.
1912    pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
1913        if !self.has_holes(arr) {
1914            return;
1915        }
1916        let moved: rustc_hash::FxHashSet<usize> =
1917            self.hole_indices(arr).into_iter().filter_map(f).collect();
1918        self.install_holes(arr, moved);
1919    }
1920
1921    /// Replace `arr`'s elision set outright, dropping the record entirely when
1922    /// the new set is empty so `has_holes` stays a single negative probe for the
1923    /// dense case.
1924    pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
1925        let Value::Obj(idx) = arr else { return };
1926        if holes.is_empty() {
1927            self.array_holes.remove(idx);
1928        } else {
1929            self.array_holes.insert(*idx, holes);
1930        }
1931    }
1932
1933    /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
1934    /// truncating `splice` leaves behind.
1935    pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
1936        self.remap_holes(arr, |i| (i < len).then_some(i));
1937    }
1938
1939    /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
1940    /// array, where each maximal run of elided positions collapses to a single
1941    /// `<N empty items>` entry. Returns the entries and whether the last of them
1942    /// is the `... N more items` tail (which the grid layout must not size a
1943    /// column to).
1944    ///
1945    /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
1946    /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
1947    /// sparse arrays print a single `<200 empty items>`.
1948    fn inspect_sparse(
1949        &self,
1950        v: &Value,
1951        items: &[Value],
1952        indent: usize,
1953        st: &mut InspectCycles,
1954    ) -> (Vec<String>, bool) {
1955        let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
1956        let empties = |n: usize| {
1957            let unit = if n == 1 { "item" } else { "items" };
1958            format!("<{n} empty {unit}>")
1959        };
1960        let mut out: Vec<String> = Vec::new();
1961        // The first index not yet accounted for by an entry.
1962        let mut index = 0usize;
1963        for (i, it) in items.iter().enumerate() {
1964            if out.len() >= MAX_ARRAY_LENGTH {
1965                break;
1966            }
1967            if holes.contains(&i) {
1968                continue;
1969            }
1970            if i > index {
1971                out.push(empties(i - index));
1972                index = i;
1973                if out.len() >= MAX_ARRAY_LENGTH {
1974                    break;
1975                }
1976            }
1977            out.push(self.inspect_lvl(it, indent + 2, st));
1978            index = i + 1;
1979        }
1980        let remaining = items.len() - index;
1981        if remaining == 0 {
1982            return (out, false);
1983        }
1984        if out.len() < MAX_ARRAY_LENGTH {
1985            // Trailing holes are still `<N empty items>`, not a truncation.
1986            out.push(empties(remaining));
1987            (out, false)
1988        } else {
1989            let unit = if remaining == 1 { "item" } else { "items" };
1990            out.push(format!("... {remaining} more {unit}"));
1991            (out, true)
1992        }
1993    }
1994    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
1995        // Integer-index keys enumerate ascending-first regardless of the order
1996        // they were supplied in (object literal, spread, Object.assign result).
1997        canonicalize_own_keys(&mut props);
1998        self.alloc(JsObj::Object(props))
1999    }
2000    pub fn as_str(&self, v: &Value) -> Option<String> {
2001        match v {
2002            Value::Str(s) => Some((**s).clone()),
2003            Value::Obj(_) => match self.get(v) {
2004                Some(JsObj::Str(s)) => Some(s.clone()),
2005                _ => None,
2006            },
2007            _ => None,
2008        }
2009    }
2010
2011    // ── scope / names ────────────────────────────────────────────────────
2012    fn frame(&self) -> &Frame {
2013        self.frames.last().unwrap()
2014    }
2015    fn cur_env(&self) -> Env {
2016        self.frame().env.clone()
2017    }
2018
2019    // ── DAP debug introspection (used only under `--dap`) ────────────────────
2020    /// Number of active call frames (the debugger's step-depth reference).
2021    pub fn frame_depth(&self) -> usize {
2022        self.frames.len()
2023    }
2024    /// Record the source line the innermost frame is executing (DAP line hook).
2025    pub fn set_cur_line(&mut self, line: u32) {
2026        if let Some(f) = self.frames.last_mut() {
2027            f.line = line;
2028        }
2029    }
2030    /// The `.stack` tail for an error created right now: one `    at <name>`
2031    /// line per live frame, innermost first, ending at the module frame.
2032    ///
2033    /// These are the REAL user frames — node-js has no `file:line:column` (the
2034    /// per-frame line is only tracked under `--dap`) and no Node-internal
2035    /// module-loader frames, so `.stack` names the call chain but can never be
2036    /// byte-identical to V8's. The names are what makes a thrown error
2037    /// diagnosable; the missing positions are documented in BUGS.md.
2038    pub fn stack_frames(&self) -> String {
2039        let mut out = String::new();
2040        for (i, f) in self.frames.iter().enumerate().rev() {
2041            let name = match (&f.owner, i) {
2042                (Some(n), _) => n.clone(),
2043                (None, 0) => "Object.<anonymous>".to_string(),
2044                (None, _) => "<anonymous>".to_string(),
2045            };
2046            out.push_str("\n    at ");
2047            out.push_str(&name);
2048        }
2049        if out.is_empty() {
2050            out.push_str("\n    at <anonymous>");
2051        }
2052        out
2053    }
2054
2055    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2056    /// `stackTrace`. `owner` carries the function name where known.
2057    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2058        self.frames
2059            .iter()
2060            .rev()
2061            .map(|f| {
2062                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2063                (name, f.line)
2064            })
2065            .collect()
2066    }
2067    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2068    pub fn dbg_locals(&self) -> Vec<(String, String)> {
2069        let env = self.cur_env();
2070        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2071        names
2072            .into_iter()
2073            .map(|n| {
2074                let v = self.read_name(&n).unwrap_or(Value::Undef);
2075                (n, self.inspect(&v))
2076            })
2077            .collect()
2078    }
2079
2080    /// Scope-chain read: local + enclosing chain, then globals.
2081    pub fn read_name(&self, name: &str) -> Option<Value> {
2082        let mut env = Some(self.cur_env());
2083        while let Some(e) = env {
2084            if let Some(v) = e.borrow().vars.get(name) {
2085                return Some(v.clone());
2086            }
2087            env = e.borrow().parent.clone();
2088        }
2089        self.globals.get(name).cloned()
2090    }
2091    pub fn read_global(&self, name: &str) -> Option<Value> {
2092        self.globals.get(name).cloned()
2093    }
2094
2095    /// Whether `name` is bound anywhere on the scope chain or in the globals —
2096    /// `read_name(..).is_some()` without cloning the value it finds. The
2097    /// strict-mode assignment path asks this and nothing else.
2098    pub fn has_name(&self, name: &str) -> bool {
2099        let mut env = Some(self.cur_env());
2100        while let Some(e) = env {
2101            if e.borrow().vars.contains_key(name) {
2102                return true;
2103            }
2104            env = e.borrow().parent.clone();
2105        }
2106        self.globals.contains_key(name)
2107    }
2108
2109    /// Assign to an existing binding up the scope chain, else create a global
2110    /// (JS assignment to an undeclared name targets the global object).
2111    /// Assign to an existing binding, or create a global. Returns `false` when
2112    /// the nearest binding is an immutable (`const`) one, which the caller turns
2113    /// into `TypeError: Assignment to constant variable.` — assigning to a
2114    /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2115    /// a mutated constant.
2116    #[must_use]
2117    pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2118        let mut env = Some(self.cur_env());
2119        while let Some(e) = env {
2120            // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2121            // binding hashed the name twice and allocated a fresh `String` key
2122            // for a key that was already there — once per assignment, so once
2123            // per loop iteration in any counting loop.
2124            //
2125            // The const check runs only at the env that OWNS the name, and the
2126            // `is_empty` guard settles the common (no consts here) case without
2127            // hashing the name again.
2128            let mut b = e.borrow_mut();
2129            if b.vars.contains_key(name) {
2130                if !b.consts.is_empty() && b.consts.contains(name) {
2131                    return false;
2132                }
2133                if let Some(slot) = b.vars.get_mut(name) {
2134                    *slot = val;
2135                }
2136                return true;
2137            }
2138            drop(b);
2139            env = e.borrow().parent.clone();
2140        }
2141        if self.global_consts.contains(name) {
2142            return false;
2143        }
2144        match self.globals.get_mut(name) {
2145            Some(slot) => *slot = val,
2146            None => {
2147                self.globals.insert(name.to_string(), val);
2148            }
2149        }
2150        true
2151    }
2152
2153    /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2154    /// plus recording the name as immutable in whichever scope received it.
2155    pub fn declare_const_name(&mut self, name: &str, val: Value) {
2156        let f = self.frame();
2157        let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2158        self.declare_name(name, val);
2159        if to_globals {
2160            self.global_consts.insert(name.to_string());
2161        } else {
2162            self.cur_env().borrow_mut().consts.insert(name.to_string());
2163        }
2164    }
2165
2166    /// Declare a new binding in the current scope (`let`/`const`). At the top of
2167    /// the module frame there is no local env, so those names become globals; once
2168    /// a block scope is open the binding belongs to that block.
2169    pub fn declare_name(&mut self, name: &str, val: Value) {
2170        let f = self.frame();
2171        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2172            self.globals.insert(name.to_string(), val);
2173        } else {
2174            self.cur_env()
2175                .borrow_mut()
2176                .vars
2177                .insert(name.to_string(), val);
2178        }
2179    }
2180
2181    /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2182    /// skips every open block scope and lands in the activation's base env.
2183    /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2184    /// name is not already bound in this activation.
2185    ///
2186    /// `var` bindings come into existence when the scope is entered, not where
2187    /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2188    /// rather than throwing. "If absent" is what keeps a parameter intact: in
2189    /// `function f(a) { var a; }` the `var` names a binding that already exists
2190    /// and must not be reset, which is also why a bare `var x;` emits nothing at
2191    /// its own position.
2192    pub fn hoist_var_name(&mut self, name: &str) {
2193        if self.frame().is_module {
2194            self.globals.entry(name.to_string()).or_insert(Value::Undef);
2195            return;
2196        }
2197        let base = self.frame().base_env.clone();
2198        let mut env = base.borrow_mut();
2199        if !env.vars.contains_key(name) {
2200            env.vars.insert(name.to_string(), Value::Undef);
2201        }
2202    }
2203
2204    pub fn declare_var_name(&mut self, name: &str, val: Value) {
2205        if self.frame().is_module {
2206            self.globals.insert(name.to_string(), val);
2207            return;
2208        }
2209        let base = self.frame().base_env.clone();
2210        base.borrow_mut().vars.insert(name.to_string(), val);
2211    }
2212
2213    /// Enter a fresh block scope.
2214    pub fn push_scope(&mut self) {
2215        let env = self.cur_env();
2216        self.frames.last_mut().unwrap().env = child_env(env);
2217    }
2218
2219    /// Leave the innermost block scope (never pops past the activation's base).
2220    pub fn pop_scope(&mut self) {
2221        let cur = self.cur_env();
2222        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2223            return;
2224        }
2225        let parent = cur.borrow().parent.clone();
2226        if let Some(p) = parent {
2227            self.frames.last_mut().unwrap().env = p;
2228        }
2229    }
2230
2231    /// Replace the innermost block scope with a fresh copy of its bindings — the
2232    /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2233    /// in one iteration keeps that iteration's value.
2234    pub fn copy_scope(&mut self) {
2235        let cur = self.cur_env();
2236        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2237            return;
2238        }
2239        let parent = cur.borrow().parent.clone();
2240        let fresh = new_env(parent);
2241        fresh.borrow_mut().vars = cur.borrow().vars.clone();
2242        self.frames.last_mut().unwrap().env = fresh;
2243    }
2244
2245    /// The current block-scope env, for save/restore across a nested chunk.
2246    pub fn scope_snapshot(&self) -> Env {
2247        self.cur_env()
2248    }
2249    pub fn restore_scope(&mut self, env: Env) {
2250        self.frames.last_mut().unwrap().env = env;
2251    }
2252    pub fn set_global(&mut self, name: &str, val: Value) {
2253        self.globals.insert(name.to_string(), val);
2254    }
2255
2256    // ── output capture ───────────────────────────────────────────────────
2257    //
2258    // Every write a *program* makes — `console.log`, `process.stdout.write`,
2259    // `print` — funnels through `write_out`, so turning capture on redirects all
2260    // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2261    // crash traceback from `main`) deliberately do not: they belong to the
2262    // process, not to the program.
2263
2264    /// Start capturing program output in-process. Any text already captured is
2265    /// discarded, so each run starts clean.
2266    pub fn begin_capture(&mut self) {
2267        self.capture = Some(Vec::new());
2268    }
2269
2270    /// Stop capturing and take everything written since [`begin_capture`],
2271    /// returning the empty string when capture was not on. The captured bytes
2272    /// are rendered lossily: this API hands back a `String`, so a program that
2273    /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2274    /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2275    ///
2276    /// [`begin_capture`]: JsHost::begin_capture
2277    /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2278    pub fn end_capture(&mut self) -> String {
2279        String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2280    }
2281
2282    /// Stop capturing and take the raw bytes, without the lossy transcription
2283    /// [`end_capture`] applies.
2284    ///
2285    /// [`end_capture`]: JsHost::end_capture
2286    pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2287        self.capture.take().unwrap_or_default()
2288    }
2289
2290    /// Whether output is being captured — the one thing a caller needs to know
2291    /// before asking the real stream a question (`isTTY`, cursor position).
2292    pub fn capturing(&self) -> bool {
2293        self.capture.is_some()
2294    }
2295
2296    /// Write program output: into the capture buffer when capturing, else to the
2297    /// process stream `stderr` selects. `s` is written verbatim — callers add
2298    /// their own line ending, as `console.log` does and `process.stdout.write`
2299    /// does not.
2300    pub fn write_out(&mut self, s: &str, stderr: bool) {
2301        self.write_out_bytes(s.as_bytes(), stderr);
2302    }
2303
2304    /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2305    /// a byte string and Node writes it through untouched, so a `Buffer` holding
2306    /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2307    /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2308    /// became seven — so the byte path exists separately from [`write_out`].
2309    ///
2310    /// [`write_out`]: JsHost::write_out
2311    pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2312        if let Some(buf) = &mut self.capture {
2313            buf.extend_from_slice(bytes);
2314            return;
2315        }
2316        use std::io::Write as _;
2317        if stderr {
2318            let mut e = std::io::stderr();
2319            let _ = e.write_all(bytes);
2320            let _ = e.flush();
2321        } else {
2322            let mut o = std::io::stdout();
2323            let _ = o.write_all(bytes);
2324            let _ = o.flush();
2325        }
2326    }
2327    pub fn del_name(&mut self, name: &str) {
2328        if self
2329            .cur_env()
2330            .borrow_mut()
2331            .vars
2332            .shift_remove(name)
2333            .is_some()
2334        {
2335            return;
2336        }
2337        self.globals.shift_remove(name);
2338    }
2339
2340    pub fn current_this(&self) -> Option<Value> {
2341        self.frame().this_obj.clone()
2342    }
2343    /// The callbacks to run for `event`, consuming any `once` registration in
2344    /// the same step — so a listener that re-emits the event cannot re-enter a
2345    /// one-shot handler.
2346    pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2347        let Some(list) = self.process_listeners.get_mut(event) else {
2348            return Vec::new();
2349        };
2350        let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2351        list.retain(|l| !l.once);
2352        fired
2353    }
2354
2355    /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2356    ///
2357    /// Node answers differently per entry point and both answers are objects:
2358    /// `node f.js` runs a CommonJS module, so top-level `this` is
2359    /// `module.exports`; `node -e` and `node -` run a Script, so it is
2360    /// `globalThis`. Verified on node v26.7.0 —
2361    /// `console.log(this === globalThis, this === module.exports)` is
2362    /// `false true` from a file and `true false` from `-e` and from stdin. It
2363    /// was `undefined` at every entry point here, so `this.x = 1` at module
2364    /// scope threw instead of populating the exports object.
2365    ///
2366    /// Only the base frame is touched: a plain function call still gets its own
2367    /// (`undefined`) binding rather than inheriting this one.
2368    pub fn set_top_this(&mut self, v: Value) {
2369        if let Some(f) = self.frames.first_mut() {
2370            f.this_obj = Some(v);
2371        }
2372    }
2373    pub fn current_env_capture(&self) -> Env {
2374        self.frame().env.clone()
2375    }
2376    pub fn current_new_target(&self) -> Option<Value> {
2377        self.frame().new_target.clone()
2378    }
2379    fn current_home_class(&self) -> Option<Value> {
2380        self.frame().home_class.clone()
2381    }
2382
2383    /// The `(parent_ctor, this_class_fields)` for a running constructor's
2384    /// `super(...)`, derived from the frame's home class.
2385    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
2386        match self.current_home_class() {
2387            Some(cv) => match self.get(&cv) {
2388                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
2389                _ => (None, Vec::new()),
2390            },
2391            None => (None, Vec::new()),
2392        }
2393    }
2394
2395    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
2396    /// by the caller, outside any host borrow) or a directly-usable value.
2397    pub fn super_resolve(&self, name: &str) -> SuperRef {
2398        let parent = match self
2399            .current_home_class()
2400            .and_then(|cv| match self.get(&cv) {
2401                Some(JsObj::Class(c)) => c.parent.clone(),
2402                _ => None,
2403            }) {
2404            Some(p) => p,
2405            None => return SuperRef::Data(Value::Undef),
2406        };
2407        let parent_proto = match self.get(&parent) {
2408            Some(JsObj::Class(pc)) => pc.proto.clone(),
2409            _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
2410        };
2411        if let Some((Some(getter), _)) = lookup_accessor(self, &parent_proto, name) {
2412            return SuperRef::Getter(getter);
2413        }
2414        SuperRef::Data(lookup_chain(self, &parent_proto, name).unwrap_or(Value::Undef))
2415    }
2416
2417    // ── signals / errors ─────────────────────────────────────────────────
2418    pub fn take_error(&mut self) -> Option<String> {
2419        self.error.take()
2420    }
2421    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
2422        let s = if msg.is_empty() {
2423            class.to_string()
2424        } else {
2425            format!("{class}: {msg}")
2426        };
2427        self.error = Some(s.clone());
2428        s
2429    }
2430}
2431
2432// ── error constructors ───────────────────────────────────────────────────────
2433
2434pub fn type_error(msg: &str) -> String {
2435    format!("TypeError: {msg}")
2436}
2437pub fn ref_error(name: &str) -> String {
2438    format!("ReferenceError: {name} is not defined")
2439}
2440pub fn range_error(msg: &str) -> String {
2441    format!("RangeError: {msg}")
2442}
2443
2444/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
2445/// largest string the engine will materialize.
2446///
2447/// Measured on node v26.7.0 (darwin arm64):
2448/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
2449/// `'a'.repeat(536870888)` succeeds with that length, and
2450/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
2451pub const MAX_STRING_LENGTH: usize = 536_870_888;
2452
2453/// The error V8 raises for a string operation whose RESULT would exceed
2454/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
2455/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
2456/// sit building a 1 TiB `String` until it was killed.
2457pub fn invalid_string_length() -> String {
2458    range_error("Invalid string length")
2459}
2460
2461/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
2462/// and 10.4.2.4 `ArraySetLength` step 3.
2463///
2464/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
2465/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
2466/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
2467/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
2468/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
2469/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
2470/// materializing four billion elements for `a.length = 2**32`.
2471pub fn to_array_length(v: &Value) -> Result<usize, String> {
2472    let n = to_number_value(v)?;
2473    // `ToUint32`: truncate toward zero, then modulo 2^32.
2474    let u = if n.is_finite() {
2475        (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
2476    } else {
2477        0
2478    };
2479    // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
2480    if (u as f64) != n {
2481        return Err(range_error("Invalid array length"));
2482    }
2483    Ok(u as usize)
2484}
2485
2486/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
2487///
2488/// `builtins::synth_error` parses that head back apart, so the bracketed code
2489/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
2490/// handling reads. Writing the head by hand at each throw site is what left a
2491/// dozen of them with `err.code === undefined` while the message matched.
2492///
2493/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
2494/// is left bracketed while the stack is captured and therefore shows up in both
2495/// `String(err)` and `err.stack` — measured on v26.7.0:
2496///
2497/// ```text
2498/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
2499/// ```
2500pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
2501    format!("{class} [{code}]: {msg}")
2502}
2503
2504/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
2505pub const CODE_MARK: &str = "\u{1}code:";
2506
2507/// A Node coded error raised from the *native* layer: `.code` is set, but the
2508/// name is never bracketed, so `String(err)` is the plain `Name: message`.
2509///
2510/// The distinction is observable and is not a stylistic choice — on v26.7.0,
2511/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
2512/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
2513/// brackets its code into the very same two reads. Encoding both through one
2514/// `Name [CODE]:` head would have to pick one and be wrong about the other.
2515///
2516/// The code rides in a marker at the head of the message rather than in the
2517/// error class, because the class text is exactly what must NOT carry it. The
2518/// marker is an internal wire format between a throw site and `synth_error`; it
2519/// never survives into a `.message`.
2520pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
2521    format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
2522}
2523
2524/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
2525/// <expected>. Received …` — Node's single most common argument rejection.
2526pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
2527    coded_error(
2528        "TypeError",
2529        "ERR_INVALID_ARG_TYPE",
2530        &format!(
2531            "The \"{name}\" {kind} must be of type {expected}. Received {}",
2532            crate::stdlib::received_desc(v)
2533        ),
2534    )
2535}
2536
2537// ── the fusevm run plumbing ──────────────────────────────────────────────────
2538
2539thread_local! {
2540    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
2541}
2542
2543/// Enable/disable DAP debug execution (`node --dap`).
2544pub fn set_debug_mode(on: bool) {
2545    DEBUG_MODE.with(|d| d.set(on));
2546}
2547
2548// ── join cycle detection ─────────────────────────────────────────────────────
2549
2550thread_local! {
2551    /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
2552    static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
2553}
2554
2555/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
2556/// it already is.
2557///
2558/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
2559/// it) is the one place the language walks an object graph with no depth bound,
2560/// so every engine cuts re-entrance here: a receiver already on the stack
2561/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
2562/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
2563/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
2564/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
2565/// uncatchable, where node returns a string.
2566///
2567/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
2568/// twice, because the first render pops before the second pushes.
2569///
2570/// A `true` return MUST be paired with [`join_stack_pop`].
2571pub fn join_stack_push(v: &Value) -> bool {
2572    match v {
2573        Value::Obj(i) => JOIN_STACK.with(|s| {
2574            let mut s = s.borrow_mut();
2575            if s.contains(i) {
2576                false
2577            } else {
2578                s.push(*i);
2579                true
2580            }
2581        }),
2582        _ => true,
2583    }
2584}
2585
2586/// Pop the innermost [`join_stack_push`].
2587pub fn join_stack_pop() {
2588    JOIN_STACK.with(|s| {
2589        s.borrow_mut().pop();
2590    });
2591}
2592
2593// ── native stack guard ───────────────────────────────────────────────────────
2594
2595thread_local! {
2596    /// Lowest stack address a nested run may start from, or 0 before the
2597    /// running thread's bounds have been measured. Cached because the pthread
2598    /// query is a syscall-free but non-trivial read and this is on every call.
2599    static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2600}
2601
2602/// Stack left unusable below the floor, as a fraction of the whole stack: the
2603/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
2604/// whatever `catch` receives it, all of which needs room *below* the deepest
2605/// call that was allowed.
2606const STACK_RESERVE_DIVISOR: usize = 8;
2607/// Floor of that reserve, for a thread whose stack is small enough that an
2608/// eighth of it would not cover the unwind.
2609const STACK_RESERVE_MIN: usize = 512 * 1024;
2610/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
2611/// large relative to a default 8 MiB stack — over-reserving costs recursion
2612/// depth, under-reserving costs the process.
2613const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
2614
2615/// The address of a local in the caller's frame — how far down the stack
2616/// execution currently is. `black_box` keeps the probe from being optimized into
2617/// a different frame.
2618fn stack_pointer() -> usize {
2619    let probe = 0u8;
2620    std::hint::black_box(&probe) as *const u8 as usize
2621}
2622
2623/// The running thread's `(lowest address, size)` stack bounds.
2624///
2625/// Asked of pthread rather than assumed, because the three threads that run JS
2626/// have three different stacks: the `node` binary's own (`main.rs` reserves
2627/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
2628/// harness thread's. A fixed byte budget would be wrong on two of the three.
2629fn stack_bounds() -> Option<(usize, usize)> {
2630    #[cfg(target_vendor = "apple")]
2631    {
2632        // SAFETY: both calls are pure reads of the calling thread's own
2633        // pthread record; neither allocates nor can fail.
2634        unsafe {
2635            let me = libc::pthread_self();
2636            let top = libc::pthread_get_stackaddr_np(me) as usize;
2637            let size = libc::pthread_get_stacksize_np(me);
2638            if size == 0 || top < size {
2639                return None;
2640            }
2641            Some((top - size, size))
2642        }
2643    }
2644    #[cfg(target_os = "linux")]
2645    {
2646        // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
2647        // read, only read on the success path, and destroyed on every path.
2648        unsafe {
2649            let mut attr: libc::pthread_attr_t = std::mem::zeroed();
2650            if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
2651                return None;
2652            }
2653            let mut low: *mut libc::c_void = std::ptr::null_mut();
2654            let mut size: libc::size_t = 0;
2655            let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
2656            libc::pthread_attr_destroy(&mut attr);
2657            if ok && size != 0 {
2658                return Some((low as usize, size));
2659            }
2660            None
2661        }
2662    }
2663    #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
2664    {
2665        None
2666    }
2667}
2668
2669/// The stack address below which a further nested VM run must throw instead of
2670/// recursing.
2671///
2672/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
2673/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
2674/// and runs the body, whose own calls land back here. Unbounded JS recursion
2675/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
2676/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
2677/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
2678/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
2679///
2680/// The floor is derived from the thread's real bounds rather than a frame count
2681/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
2682/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
2683/// stack and no more), a release build far less, and a native builtin recursing
2684/// through a user callback spends a different amount again.
2685fn stack_floor() -> usize {
2686    let cached = STACK_FLOOR.with(|c| c.get());
2687    if cached != 0 {
2688        return cached;
2689    }
2690    let floor = match stack_bounds() {
2691        Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
2692        None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
2693    };
2694    STACK_FLOOR.with(|c| c.set(floor));
2695    floor
2696}
2697
2698/// Stack given to each generator/async coroutine.
2699///
2700/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
2701/// left a `function*` body barely ten frames of recursion before it walked off
2702/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
2703/// a larger one is address space, not resident memory — but it IS per live
2704/// generator, so this stays far below the entry thread's
2705/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
2706/// has thousands of these.
2707const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
2708
2709/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
2710fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
2711    stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
2712}
2713
2714/// corosensei's own `DefaultStack::default()` size, used only when the
2715/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
2716/// on a stack whose bounds are not ours to read.
2717const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
2718
2719/// Give a coroutine whose stack bounds are unknown a floor measured from where
2720/// its body starts. Called once, at body entry, on the coroutine's own stack.
2721fn ensure_coroutine_floor() {
2722    if STACK_FLOOR.with(|c| c.get()) != 0 {
2723        return;
2724    }
2725    let budget = CORO_FALLBACK_STACK_SIZE
2726        - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
2727    STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
2728}
2729
2730/// Install `floor` as the current stack floor, returning the previous one.
2731///
2732/// Used around a coroutine resume, which switches to a stack the thread's
2733/// pthread record knows nothing about. A floor of 0 means "not known" and makes
2734/// the next [`stack_floor`] measure again, which is the right answer for the
2735/// entry thread and a conservative one for a fallback coroutine stack.
2736fn swap_stack_floor(floor: usize) -> usize {
2737    STACK_FLOOR.with(|c| c.replace(floor))
2738}
2739
2740/// Whether the native stack is too close to its floor for one more nested run.
2741pub fn stack_exhausted() -> bool {
2742    stack_pointer() <= stack_floor()
2743}
2744
2745/// The error V8 raises when the call stack is exhausted. Catchable, and with the
2746/// `RangeError` constructor node uses — not a `panic!`.
2747pub fn stack_overflow_error() -> String {
2748    range_error("Maximum call stack size exceeded")
2749}
2750
2751/// Pool key for the body of user function `def_id`.
2752pub fn func_key(def_id: usize) -> u64 {
2753    1 << 40 | def_id as u64
2754}
2755
2756/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
2757/// handler, 2 = the finalizer.
2758pub fn try_key(try_id: usize, part: u64) -> u64 {
2759    2 << 40 | (try_id as u64) << 2 | part
2760}
2761
2762thread_local! {
2763    /// VMs that have finished a run, kept for the next one — grouped by the
2764    /// chunk they still hold.
2765    ///
2766    /// Every JS call, every `try` block and every generator step runs its chunk
2767    /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
2768    /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
2769    /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
2770    /// 400k calls, so it built 400k VMs to run 23 ops each.
2771    ///
2772    /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
2773    /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
2774    /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
2775    /// call takes back the VM that already holds that body and copies nothing:
2776    /// `VM::reset` is handed the chunk the VM was already carrying.
2777    ///
2778    /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
2779    /// recycled VM needs none of that again. Each key holds a stack of VMs, and
2780    /// a nested (or recursive) call takes the next one, so a key grows to the
2781    /// deepest simultaneous entry into that function and no further.
2782    static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
2783        RefCell::new(rustc_hash::FxHashMap::default());
2784}
2785
2786/// An idle VM filed under `key`, if any.
2787fn take_pooled(key: u64) -> Option<VM> {
2788    VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
2789}
2790
2791/// File a finished VM under `key` for the next run to take.
2792fn put_pooled(key: u64, vm: VM) {
2793    VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
2794}
2795
2796/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
2797/// and fitted with the builtins and hooks a fresh VM needs.
2798fn acquire_vm(chunk: Chunk) -> VM {
2799    if let Some(mut vm) = take_pooled(0) {
2800        vm.reset(chunk);
2801        return vm;
2802    }
2803    let mut vm = VM::new(chunk);
2804    crate::builtins::install(&mut vm);
2805    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
2806        crate::builtins::numeric_hook(op, a, b)
2807    }));
2808    // Under `--dap` the tracing JIT would compile hot loops and skip the
2809    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
2810    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
2811    // extension seam mirrors pythonrs should the marker emission ever switch.
2812    // The mode is fixed before the first chunk runs, so a pooled VM can never
2813    // come back wearing the wrong one.
2814    if DEBUG_MODE.with(|d| d.get()) {
2815        vm.set_extension_handler(Box::new(|vm, id, _| {
2816            crate::dap::on_ext(vm, id);
2817        }));
2818    } else {
2819        vm.enable_tracing_jit();
2820    }
2821    vm
2822}
2823
2824/// Register every node-js builtin + the numeric hook on a VM, then run it.
2825///
2826/// For a chunk that runs once — a module body, an `eval` — there is nothing to
2827/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
2828/// that runs repeatedly (a function body, a `try` block) goes through
2829/// [`run_chunk_keyed`] instead and never copies its chunk twice.
2830pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
2831    // Checked before the `VM` is built: `VM::new` + `install` are themselves
2832    // several KiB of frame, so a check after them could already have overflowed.
2833    if stack_exhausted() {
2834        return Err(stack_overflow_error());
2835    }
2836    finish_run(0, acquire_vm(chunk))
2837}
2838
2839/// Run the chunk filed under `key`, building it with `make` only if no VM is
2840/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
2841/// call copies no bytecode at all.
2842pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
2843    if stack_exhausted() {
2844        return Err(stack_overflow_error());
2845    }
2846    let vm = match take_pooled(key) {
2847        Some(mut vm) => {
2848            // Hand the VM back the chunk it is already carrying: `reset` takes
2849            // an owned `Chunk`, and this is the one place where the owned chunk
2850            // costs nothing.
2851            let held = std::mem::take(&mut vm.chunk);
2852            vm.reset(held);
2853            vm
2854        }
2855        None => acquire_vm(make()),
2856    };
2857    finish_run(key, vm)
2858}
2859
2860/// Run a prepared VM to completion and file it back under `key`.
2861fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
2862    let outcome = vm.run();
2863    let result = match outcome {
2864        _ if with_host(|h| h.error.is_some()) => {
2865            Err(with_host(|h| h.take_error()).expect("just checked"))
2866        }
2867        VMResult::Ok(v) => Ok(v),
2868        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
2869        VMResult::Error(e) => Err(e),
2870    };
2871    put_pooled(key, vm);
2872    result
2873}
2874
2875/// Run `chunk` in the GLOBAL scope instead of the caller's.
2876///
2877/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
2878/// and can shadow — the *calling function's* locals. That is right for a direct
2879/// `eval`, and wrong for every other runtime-source construct: a `new Function`
2880/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
2881/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
2882/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
2883/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
2884/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
2885/// is `"undefined"` there and was `"number"` here.
2886///
2887/// A `var` the chunk itself declares lands in the top-level scope and persists,
2888/// so successive `vm.runInThisContext` calls share it.
2889pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
2890    let global_env = with_host(|h| h.global_env.clone());
2891    with_host(|h| {
2892        h.frames.push(Frame {
2893            env: global_env.clone(),
2894            base_env: global_env,
2895            this_obj: None,
2896            new_target: None,
2897            home_class: None,
2898            line: 0,
2899            owner: None,
2900            is_module: true,
2901        })
2902    });
2903    let r = run_chunk_on(chunk);
2904    with_host(|h| {
2905        h.frames.pop();
2906    });
2907    r
2908}
2909
2910/// Run the top-level program chunk, then drain the event loop (microtasks +
2911/// timers) until quiescent — matching Node, which keeps the process alive while
2912/// pending async work remains.
2913pub fn run_main(chunk: Chunk) -> Result<Value, String> {
2914    let r = run_chunk_on(chunk);
2915    with_host(|h| h.signal = None);
2916    if r.is_ok() {
2917        run_event_loop()?;
2918        finish_process_events()?;
2919    }
2920    r
2921}
2922
2923/// The shutdown sequence Node runs once the loop has drained on its own: fire
2924/// `beforeExit` (which MAY schedule more work, in which case the loop runs
2925/// again and `beforeExit` fires again), then fire `exit` exactly once.
2926///
2927/// Neither event fired at all before this existed, so `process.on('exit', …)`
2928/// was a registration with no delivery — a listener whose body printed was
2929/// silently dropped, and one that set `process.exitCode` could not affect the
2930/// status. Measured on node v26.7.0,
2931/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
2932///
2933/// An explicit `process.exit()` never reaches here (it leaves the process from
2934/// inside the builtin), and neither does an uncaught exception — matching
2935/// Node, where `beforeExit` is skipped on both paths.
2936fn finish_process_events() -> Result<(), String> {
2937    // Bounded: a `beforeExit` listener that re-arms work every time would spin
2938    // forever, exactly as it does in Node, but a runaway here would hang a
2939    // parity run with no output, so it is capped and then treated as drained.
2940    for _ in 0..1000 {
2941        let code = with_host(|h| h.exit_code).unwrap_or(0);
2942        if !crate::stdlib::process::emit_before_exit(code)? {
2943            break;
2944        }
2945        let more =
2946            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
2947        if !more {
2948            break;
2949        }
2950        run_event_loop()?;
2951    }
2952    let code = with_host(|h| h.exit_code).unwrap_or(0);
2953    crate::stdlib::process::emit_exit_event(code)
2954}
2955
2956// ── formatting ───────────────────────────────────────────────────────────────
2957
2958/// Format a JS number exactly as `Number.prototype.toString` does for the common
2959/// range (no exponential-notation threshold handling for very large/small).
2960pub fn fmt_number(f: f64) -> String {
2961    if f.is_nan() {
2962        return "NaN".into();
2963    }
2964    if f.is_infinite() {
2965        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
2966    }
2967    if f == 0.0 {
2968        // Covers -0.0 too: (-0).toString() === "0".
2969        return "0".into();
2970    }
2971    if f < 0.0 {
2972        return format!("-{}", js_number_repr(-f));
2973    }
2974    js_number_repr(f)
2975}
2976
2977/// If `k` is an array-index property key, return its numeric value. Per
2978/// ECMAScript, a String property key `P` is an array index iff
2979/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
2980/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
2981pub fn array_index(k: &str) -> Option<u32> {
2982    if k.is_empty() {
2983        return None;
2984    }
2985    if k == "0" {
2986        return Some(0);
2987    }
2988    // A leading '0' (other than the lone "0" above) is non-canonical.
2989    if k.as_bytes()[0] == b'0' {
2990        return None;
2991    }
2992    if !k.bytes().all(|b| b.is_ascii_digit()) {
2993        return None;
2994    }
2995    match k.parse::<u64>() {
2996        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
2997        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
2998        _ => None,
2999    }
3000}
3001
3002/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3003/// integer-index keys sort ascending-numeric and precede all string keys; two
3004/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3005/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3006/// they also fall into the stable-insertion-order tail.)
3007pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3008    use std::cmp::Ordering;
3009    match (array_index(a), array_index(b)) {
3010        (Some(x), Some(y)) => x.cmp(&y),
3011        (Some(_), None) => Ordering::Less,
3012        (None, Some(_)) => Ordering::Greater,
3013        (None, None) => Ordering::Equal,
3014    }
3015}
3016
3017/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3018/// place: array-index keys ascending first, then the remaining keys in their
3019/// existing (insertion) order. A no-op unless at least one index key is present,
3020/// so the overwhelmingly common all-string-key object keeps its exact order and
3021/// pays nothing. `IndexMap::sort_by` is a stable sort.
3022pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3023    if props.keys().any(|k| array_index(k).is_some()) {
3024        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3025    }
3026}
3027
3028/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3029///
3030/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3031/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3032/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3033/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3034/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3035/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3036/// `n > 21` or `n ≤ -6`.
3037fn js_number_repr(a: f64) -> String {
3038    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3039    // round-trip digits. Split it into the digit string `s` and exponent `E`.
3040    let sci = format!("{a:e}");
3041    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3042    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3043    let s: String = mant.chars().filter(|c| *c != '.').collect();
3044    let k = s.len() as i32; // number of significant digits
3045    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3046
3047    if k <= n && n <= 21 {
3048        // Integer with trailing zeros: all digits, then n−k zeros.
3049        let mut out = s;
3050        out.push_str(&"0".repeat((n - k) as usize));
3051        out
3052    } else if 0 < n && n <= 21 {
3053        // Decimal point inside the digit run: n digits, '.', the rest.
3054        format!("{}.{}", &s[..n as usize], &s[n as usize..])
3055    } else if -6 < n && n <= 0 {
3056        // Leading "0." then (−n) zeros then all digits.
3057        format!("0.{}{}", "0".repeat((-n) as usize), s)
3058    } else {
3059        // Exponential form. Exponent digit is n−1, always signed.
3060        let exp = n - 1;
3061        let sign = if exp >= 0 { '+' } else { '-' };
3062        let mag = exp.abs();
3063        if k == 1 {
3064            format!("{s}e{sign}{mag}")
3065        } else {
3066            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3067        }
3068    }
3069}
3070
3071impl JsHost {
3072    /// The `typeof` string for `v`.
3073    pub fn type_of(&self, v: &Value) -> &'static str {
3074        match v {
3075            Value::Undef => "undefined",
3076            Value::Bool(_) => "boolean",
3077            Value::Int(_) | Value::Float(_) => "number",
3078            Value::Str(_) => "string",
3079            Value::Obj(_) => match self.get(v) {
3080                Some(JsObj::Str(_)) => "string",
3081                // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3082                // target is callable, so `typeof` classifies by the target —
3083                // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3084                // is bounded: a proxy of a proxy defers again.
3085                Some(JsObj::Proxy { target, .. }) => {
3086                    let mut cur = target;
3087                    for _ in 0..100 {
3088                        match self.get(cur) {
3089                            Some(JsObj::Proxy { target: t, .. }) => cur = t,
3090                            _ => break,
3091                        }
3092                    }
3093                    if is_callable(self, cur) {
3094                        "function"
3095                    } else {
3096                        "object"
3097                    }
3098                }
3099                Some(JsObj::Func(_))
3100                | Some(JsObj::BoundMethod { .. })
3101                | Some(JsObj::BoundFunc { .. })
3102                | Some(JsObj::Class(_)) => "function",
3103                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3104                // `typeof === "function"` — EXCEPT the non-callable namespace
3105                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3106                Some(JsObj::Builtin(n)) => {
3107                    const NON_CALLABLE_NS: &[&str] = &[
3108                        // The live `require.cache` view is a plain object to a
3109                        // script, not something it can call.
3110                        crate::builtins::REQUIRE_CACHE,
3111                        "Math",
3112                        "JSON",
3113                        "console",
3114                        "Reflect",
3115                        "process",
3116                        "Atomics",
3117                        "performance",
3118                        "fs",
3119                        "path",
3120                        "os",
3121                        "util",
3122                        "crypto",
3123                        "querystring",
3124                        "events",
3125                        "stream",
3126                        "timers",
3127                        "perf_hooks",
3128                        "async_hooks",
3129                        "diagnostics_channel",
3130                        "v8",
3131                        "dns",
3132                        "punycode",
3133                        "child_process",
3134                        "tty",
3135                        "url",
3136                        "zlib",
3137                        "string_decoder",
3138                        "assert",
3139                        "http",
3140                        "net",
3141                        "buffer",
3142                    ];
3143                    if NON_CALLABLE_NS.contains(&n.as_str()) {
3144                        "object"
3145                    } else {
3146                        "function"
3147                    }
3148                }
3149                Some(JsObj::Symbol { .. }) => "symbol",
3150                Some(JsObj::BigInt(_)) => "bigint",
3151                _ => "object", // arrays, objects, null, Map/Set, generators
3152            },
3153            _ => "object",
3154        }
3155    }
3156
3157    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3158    pub fn truthy(&self, v: &Value) -> bool {
3159        match v {
3160            Value::Undef => false,
3161            Value::Bool(b) => *b,
3162            Value::Int(n) => *n != 0,
3163            Value::Float(f) => *f != 0.0 && !f.is_nan(),
3164            Value::Str(s) => !s.is_empty(),
3165            Value::Obj(_) => match self.get(v) {
3166                Some(JsObj::Str(s)) => !s.is_empty(),
3167                Some(JsObj::Null) => false,
3168                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3169                _ => true, // arrays, objects, functions
3170            },
3171            _ => true,
3172        }
3173    }
3174
3175    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3176    pub fn to_number(&self, v: &Value) -> f64 {
3177        match v {
3178            Value::Undef => f64::NAN,
3179            Value::Bool(b) => {
3180                if *b {
3181                    1.0
3182                } else {
3183                    0.0
3184                }
3185            }
3186            Value::Int(n) => *n as f64,
3187            Value::Float(f) => *f,
3188            Value::Str(s) => str_to_number(s),
3189            Value::Obj(_) => match self.get(v) {
3190                Some(JsObj::Str(s)) => str_to_number(s),
3191                Some(JsObj::Null) => 0.0,
3192                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3193                Some(JsObj::Array(items)) => {
3194                    // [] -> 0, [x] -> ToNumber(x), else NaN.
3195                    if items.is_empty() {
3196                        0.0
3197                    } else if items.len() == 1 {
3198                        self.to_number(&items[0])
3199                    } else {
3200                        f64::NAN
3201                    }
3202                }
3203                _ => f64::NAN,
3204            },
3205            _ => f64::NAN,
3206        }
3207    }
3208
3209    /// `String(v)` — the string-coercion form (raw, unquoted).
3210    pub fn str_of(&self, v: &Value) -> String {
3211        match v {
3212            Value::Undef => "undefined".into(),
3213            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3214            Value::Int(n) => n.to_string(),
3215            Value::Float(f) => fmt_number(*f),
3216            Value::Str(s) => (**s).clone(),
3217            Value::Obj(_) => match self.get(v) {
3218                Some(JsObj::Str(s)) => s.clone(),
3219                Some(JsObj::Null) => "null".into(),
3220                Some(JsObj::BigInt(b)) => b.to_string(),
3221                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3222                Some(JsObj::Array(items)) => {
3223                    // Array.prototype.toString: comma-join, null/undefined -> "".
3224                    // Guarded by the JoinStack (see `join_stack_push`) so a
3225                    // self-referential array yields "" instead of recursing until
3226                    // the native stack aborts the process.
3227                    if !join_stack_push(v) {
3228                        return String::new();
3229                    }
3230                    let parts: Vec<String> = items
3231                        .iter()
3232                        .map(|x| match x {
3233                            Value::Undef => String::new(),
3234                            _ if self.is_null(x) => String::new(),
3235                            _ => self.str_of(x),
3236                        })
3237                        .collect();
3238                    join_stack_pop();
3239                    parts.join(",")
3240                }
3241                Some(JsObj::Object(props)) => {
3242                    // A native `Buffer` stringifies to its decoded (utf-8)
3243                    // contents, matching `buf.toString()` — needed for `'' + buf`,
3244                    // template interpolation, and `data += chunk` (the pattern
3245                    // Express/body-parser use to read a request body).
3246                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3247                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3248                            Some(JsObj::Array(items)) => {
3249                                items.iter().map(|x| self.to_number(x) as u8).collect()
3250                            }
3251                            _ => Vec::new(),
3252                        };
3253                        String::from_utf8_lossy(&bytes).into_owned()
3254                    } else if let Some(s) = self.error_to_string(v) {
3255                        s
3256                    } else {
3257                        "[object Object]".into()
3258                    }
3259                }
3260                Some(JsObj::Func(f)) => {
3261                    // A function built from runtime source (`new Function`,
3262                    // `vm.compileFunction`) retains the exact text V8 synthesizes
3263                    // for it, so `Function.prototype.toString` reports what Node
3264                    // reports. Ordinary functions carry no source here (the
3265                    // compiler keeps no spans), so they fall back to a placeholder.
3266                    if let Some(src) = self.fn_prop(v, "@@source") {
3267                        return self.str_of(&src);
3268                    }
3269                    let name = self
3270                        .funcs
3271                        .get(f.def_id)
3272                        .map(|d| d.name.clone())
3273                        .unwrap_or_default();
3274                    format!("function {name}() {{ [code] }}")
3275                }
3276                Some(JsObj::Builtin(n)) => format!("function {n}() {{ [native code] }}"),
3277                Some(JsObj::BoundMethod { .. }) | Some(JsObj::BoundFunc { .. }) => {
3278                    "function () { [native code] }".into()
3279                }
3280                // `Function.prototype.toString` refuses to expose a proxy's
3281                // target: V8 reports the native-code form for a proxy of ANY
3282                // callable, so `String(new Proxy(function f(){}, {}))` is
3283                // `function () { [native code] }`, not `f`'s source.
3284                Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
3285                    "function () { [native code] }".into()
3286                }
3287                Some(JsObj::Class(c)) => format!("class {} {{ }}", c.name),
3288                Some(JsObj::Symbol { desc, .. }) => {
3289                    // `String(sym)` is allowed (unlike implicit coercion) and yields
3290                    // `Symbol(desc)`.
3291                    match desc {
3292                        Some(d) => format!("Symbol({d})"),
3293                        None => "Symbol()".into(),
3294                    }
3295                }
3296                _ => "[object Object]".into(),
3297            },
3298            _ => "[object Object]".into(),
3299        }
3300    }
3301
3302    /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
3303    /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
3304    /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
3305    /// entry and showing it twice would be wrong.
3306    ///
3307    /// Only a DATA property is seen. A tag supplied by a prototype getter
3308    /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
3309    /// which cannot run under the host borrow `inspect` holds — such an object
3310    /// prints without the prefix.
3311    fn inspect_tag(&self, v: &Value) -> Option<String> {
3312        let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
3313        if own && self.prop_attrs(v, "@@toStringTag").enumerable {
3314            return None;
3315        }
3316        let t = lookup_chain(self, v, "@@toStringTag")?;
3317        self.as_str(&t)
3318    }
3319
3320    /// `console.log`-style rendering of a top-level argument: bare strings print
3321    /// raw; everything else uses `inspect`.
3322    pub fn console_format(&self, v: &Value) -> String {
3323        match v {
3324            Value::Str(_) => self.str_of(v),
3325            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
3326            _ => self.inspect(v),
3327        }
3328    }
3329
3330    /// `util.inspect`-style rendering (nested; strings quoted).
3331    pub fn inspect(&self, v: &Value) -> String {
3332        self.inspect_lvl(v, 0, &mut InspectCycles::default())
3333    }
3334
3335    /// `util.inspect` at a given indentation level, with the cycle guard applied
3336    /// around the object cases.
3337    ///
3338    /// A value already being rendered further up the chain is a CYCLE, and Node
3339    /// marks both ends of it: the back-edge prints `[Circular *N]` and the
3340    /// object it points back at is prefixed `<ref *N>`. Without this the walk
3341    /// only stopped when the depth limit turned the back-edge into `[Object]`,
3342    /// so `const c={a:1}; c.c=c` printed the misleading
3343    /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
3344    /// `<ref *1> { a: 1, c: [Circular *1] }`.
3345    ///
3346    /// The `*N` id is only assigned when the back-edge is reached, i.e. while
3347    /// the target's own children are being rendered — so the prefix can only be
3348    /// decided after `inspect_value` returns.
3349    fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
3350        if !matches!(v, Value::Obj(_)) {
3351            return self.inspect_value(v, indent, st);
3352        }
3353        if st.seen.iter().any(|p| self.strict_eq(p, v)) {
3354            return format!("[Circular *{}]", st.mark(self, v));
3355        }
3356        st.seen.push(v.clone());
3357        let body = self.inspect_value(v, indent, st);
3358        st.seen.pop();
3359        match st.id_of(self, v) {
3360            Some(id) => format!("<ref *{id}> {body}"),
3361            None => body,
3362        }
3363    }
3364
3365    /// The rendering itself, once `inspect_lvl` has established that `v` is not
3366    /// a back-edge into an object already on the stack.
3367    fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
3368        match v {
3369            Value::Undef => "undefined".into(),
3370            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3371            Value::Int(n) => n.to_string(),
3372            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
3373            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
3374            Value::Float(f) => fmt_number(*f),
3375            Value::Str(s) => quote_str(s),
3376            Value::Obj(_) => match self.get(v) {
3377                Some(JsObj::Str(s)) => quote_str(s),
3378                Some(JsObj::Null) => "null".into(),
3379                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
3380                Some(JsObj::BigInt(b)) => format!("{b}n"),
3381                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3382                // `util.inspect` on node v26.7.0 renders a proxy as
3383                // `Proxy(<target>)` — the target's own rendering, wrapped. It
3384                // deliberately does NOT run the handler's traps, so this stays a
3385                // pure `&self` read like every other inspect arm.
3386                Some(JsObj::Proxy { target, .. }) => {
3387                    format!("Proxy({})", self.inspect_lvl(target, indent, st))
3388                }
3389                Some(JsObj::Array(items)) => {
3390                    // Own enumerable non-index string props (e.g. a `str.match(re)`
3391                    // result's `index`/`input`/`groups`, or a user-assigned
3392                    // `arr.foo`) render after the elements, as `key: value`.
3393                    let prop_keys: Vec<String> = self
3394                        .fn_prop_keys(v)
3395                        .into_iter()
3396                        .filter(|k| {
3397                            !k.starts_with("@@")
3398                                && !k.starts_with('#')
3399                                && self.prop_attrs(v, k).enumerable
3400                        })
3401                        .collect();
3402                    // An own enumerable SYMBOL-keyed property renders after the
3403                    // string keys as `Symbol(desc): value`, as it does on an
3404                    // object receiver.
3405                    let sym_entries = self.own_symbol_entries(v);
3406                    if items.is_empty() && prop_keys.is_empty() && sym_entries.is_empty() {
3407                        return "[]".into();
3408                    }
3409                    // Node's default inspect depth is 2 (root = depth 0); deeper
3410                    // nesting collapses to `[Array]`. indent grows by 2 per level.
3411                    if indent > 2 * inspect_max_depth() {
3412                        return "[Array]".into();
3413                    }
3414                    // `util.inspect`'s `maxArrayLength` (default 100): only the
3415                    // first 100 elements are formatted, and the rest collapse to
3416                    // a `... N more items` entry. Without the cap a 120-element
3417                    // array printed all 120 — and, because the grid column width
3418                    // is computed from what is SHOWN, every column was also one
3419                    // character wider than node's.
3420                    // A SPARSE array takes node's `formatSpecialArray` path: an
3421                    // elided run renders as `<N empty items>` rather than as the
3422                    // `undefined` it reads back as.
3423                    let (mut inner, has_tail) = if self.has_holes(v) {
3424                        self.inspect_sparse(v, items, indent, st)
3425                    } else {
3426                        let shown = items.len().min(MAX_ARRAY_LENGTH);
3427                        let mut inner: Vec<String> = items[..shown]
3428                            .iter()
3429                            .map(|x| self.inspect_lvl(x, indent + 2, st))
3430                            .collect();
3431                        let remaining = items.len() - shown;
3432                        if remaining > 0 {
3433                            let unit = if remaining == 1 { "item" } else { "items" };
3434                            inner.push(format!("... {remaining} more {unit}"));
3435                        }
3436                        (inner, remaining > 0)
3437                    };
3438                    let has_props = !prop_keys.is_empty() || !sym_entries.is_empty();
3439                    for k in &prop_keys {
3440                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
3441                        inner.push(format!(
3442                            "{}: {}",
3443                            fmt_key(k),
3444                            self.inspect_lvl(&val, indent + 2, st)
3445                        ));
3446                    }
3447                    for (k, val) in &sym_entries {
3448                        let label = match self.symbol_of_key(k) {
3449                            Some(s) => self.inspect(&s),
3450                            None => continue,
3451                        };
3452                        inner.push(format!(
3453                            "{label}: {}",
3454                            self.inspect_lvl(val, indent + 2, st)
3455                        ));
3456                    }
3457                    self.render_array(&inner, items, indent, has_props, has_tail, "")
3458                }
3459                // `URLSearchParams` renders its pairs, not its slots:
3460                // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
3461                // which is why it is a pair list rather than a Map rendering.
3462                Some(JsObj::Object(props))
3463                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
3464                        == Some("URLSearchParams") =>
3465                {
3466                    let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
3467                        Some(JsObj::Array(items)) => items.clone(),
3468                        _ => Vec::new(),
3469                    };
3470                    if pairs.is_empty() {
3471                        return "URLSearchParams {}".into();
3472                    }
3473                    let inner: Vec<String> = pairs
3474                        .iter()
3475                        .filter_map(|kv| match self.get(kv) {
3476                            Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
3477                                "{} => {}",
3478                                self.inspect_lvl(&p[0], indent + 2, st),
3479                                self.inspect_lvl(&p[1], indent + 2, st)
3480                            )),
3481                            _ => None,
3482                        })
3483                        .collect();
3484                    self.render_object(&inner, "URLSearchParams ", indent)
3485                }
3486                // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
3487                // constructor and length, then the elements laid out exactly as
3488                // an array's. Without this it fell through to the generic object
3489                // arm and printed the `{ length, byteLength, byteOffset,
3490                // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
3491                // which is the whole reason anyone logs one.
3492                Some(JsObj::Object(props))
3493                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
3494                        == Some("TypedArray") =>
3495                {
3496                    let kind = props
3497                        .get("@@kind")
3498                        .map(|k| self.str_of(k))
3499                        .unwrap_or_else(|| "TypedArray".into());
3500                    let elems: Vec<Value> = match props.get("@@elems").and_then(|e| self.get(e)) {
3501                        Some(JsObj::Array(items)) => items.clone(),
3502                        _ => Vec::new(),
3503                    };
3504                    let base = format!("{kind}({}) ", elems.len());
3505                    if indent > 2 * inspect_max_depth() {
3506                        return format!("[{kind}]");
3507                    }
3508                    let shown = elems.len().min(MAX_ARRAY_LENGTH);
3509                    let mut inner: Vec<String> = elems[..shown]
3510                        .iter()
3511                        .map(|x| self.inspect_lvl(x, indent + 2, st))
3512                        .collect();
3513                    let remaining = elems.len() - shown;
3514                    if remaining > 0 {
3515                        let unit = if remaining == 1 { "item" } else { "items" };
3516                        inner.push(format!("... {remaining} more {unit}"));
3517                    }
3518                    self.render_array(&inner, &elems, indent, false, remaining > 0, &base)
3519                }
3520                // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
3521                // at 50 with a `... N more byte(s)` tail, exactly as
3522                // `util.inspect` does. Without this a `console.log(buf)` (the
3523                // single most common thing anyone does with a Buffer) printed
3524                // the internal `{ length, byteLength, … }` bookkeeping.
3525                Some(JsObj::Object(props))
3526                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
3527                        == Some("Buffer") =>
3528                {
3529                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3530                        Some(JsObj::Array(items)) => {
3531                            items.iter().map(|x| self.to_number(x) as u8).collect()
3532                        }
3533                        _ => Vec::new(),
3534                    };
3535                    const MAX: usize = 50;
3536                    let shown: Vec<String> =
3537                        bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
3538                    let mut out = format!("<Buffer {}", shown.join(" "));
3539                    if bytes.len() > MAX {
3540                        let more = bytes.len() - MAX;
3541                        let unit = if more == 1 { "byte" } else { "bytes" };
3542                        out.push_str(&format!(" ... {more} more {unit}"));
3543                    }
3544                    out.push('>');
3545                    out
3546                }
3547                // An Error inspects as its `.stack` — never as an object literal
3548                // exposing the internal `message`/`stack` slots. Any own property
3549                // a script added beyond those follows in braces, as V8 renders
3550                // it: `Error: x\n    at … { code: 'C' }`.
3551                Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
3552                    let stack = lookup_chain(self, v, "stack")
3553                        .map(|s| self.str_of(&s))
3554                        .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
3555                    let extra: Vec<String> = self
3556                        .own_enum_key_names(v)
3557                        .into_iter()
3558                        .filter(|k| k != "name")
3559                        .map(|k| {
3560                            let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
3561                                Some(JsObj::Object(p)) => {
3562                                    p.get(&k).cloned().unwrap_or(Value::Undef)
3563                                }
3564                                _ => Value::Undef,
3565                            });
3566                            format!(
3567                                "{}: {}",
3568                                fmt_key(&k),
3569                                self.inspect_lvl(&val, indent + 2, st)
3570                            )
3571                        })
3572                        .collect();
3573                    if extra.is_empty() {
3574                        stack
3575                    } else {
3576                        format!("{stack} {{ {} }}", extra.join(", "))
3577                    }
3578                }
3579                Some(JsObj::Object(props)) => {
3580                    // Instances print with their constructor name as a prefix
3581                    // (`C { x: 1 }`); plain objects have none; a null-prototype
3582                    // object (e.g. an `Object.groupBy` result) is tagged
3583                    // `[Object: null prototype]`.
3584                    let ctor = match self.ctor_name(v) {
3585                        n if n.is_empty() => "Object".to_string(),
3586                        n => n,
3587                    };
3588                    let plain_prefix = if ctor == "Object" {
3589                        String::new()
3590                    } else {
3591                        format!("{ctor} ")
3592                    };
3593                    let prefix = if self.has_null_proto(v) {
3594                        "[Object: null prototype] ".to_string()
3595                    } else {
3596                        // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
3597                        match self.inspect_tag(v) {
3598                            Some(t) if t != ctor => format!("{ctor} [{t}] "),
3599                            _ => plain_prefix.clone(),
3600                        }
3601                    };
3602                    // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
3603                    // private class fields; a real symbol-keyed own property is a
3604                    // visible one and renders as `Symbol(desc): value`.
3605                    // An own ACCESSOR has no value to print: node shows the
3606                    // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
3607                    // place. It is found through the `@@ord:` marker the
3608                    // property map holds for it, which is also what puts it in
3609                    // declaration order among the data properties. Without this
3610                    // an accessor rendered as nothing at all — `{ get z(){} }`
3611                    // printed `{}`.
3612                    let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
3613                        .iter()
3614                        .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
3615                            Some(real) => {
3616                                let attrs = self.prop_attrs(v, real);
3617                                let label = match self.own_accessor(v, real)? {
3618                                    (Some(_), Some(_)) => "[Getter/Setter]",
3619                                    (Some(_), None) => "[Getter]",
3620                                    (None, Some(_)) => "[Setter]",
3621                                    (None, None) => return None,
3622                                };
3623                                attrs.enumerable.then(|| (fmt_key(real), Err(label)))
3624                            }
3625                            // Only an ENUMERABLE own property is shown, as node
3626                            // does: a native instance keeps bookkeeping (a
3627                            // `URLSearchParams`'s `size`) as a hidden own slot,
3628                            // and printing it would report a spec getter as data.
3629                            None if !k.starts_with("@@")
3630                                && !k.starts_with('#')
3631                                && self.prop_attrs(v, k).enumerable =>
3632                            {
3633                                Some((fmt_key(k), Ok(val)))
3634                            }
3635                            None => None,
3636                        })
3637                        .collect();
3638                    shown.extend(props.iter().filter_map(|(k, val)| {
3639                        let sym = self.symbol_of_key(k)?;
3640                        self.prop_attrs(v, k)
3641                            .enumerable
3642                            .then(|| (self.inspect(&sym), Ok(val)))
3643                    }));
3644                    if shown.is_empty() {
3645                        return format!("{prefix}{{}}");
3646                    }
3647                    // Depth limit (Node default 2): deeper objects collapse to
3648                    // `[Object]` (or `[ClassName]` for a named instance).
3649                    if indent > 2 * inspect_max_depth() {
3650                        return if self.has_null_proto(v) {
3651                            // Already bracketed (`[Object: null prototype]`).
3652                            prefix.trim_end().to_string()
3653                        } else if plain_prefix.is_empty() {
3654                            "[Object]".into()
3655                        } else {
3656                            format!("[{}]", plain_prefix.trim_end())
3657                        };
3658                    }
3659                    let inner: Vec<String> = shown
3660                        .iter()
3661                        .map(|(k, val)| match val {
3662                            Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
3663                            Err(label) => format!("{k}: {label}"),
3664                        })
3665                        .collect();
3666                    self.render_object(&inner, &prefix, indent)
3667                }
3668                Some(JsObj::Symbol { desc, .. }) => match desc {
3669                    Some(d) => format!("Symbol({d})"),
3670                    None => "Symbol()".into(),
3671                },
3672                Some(JsObj::Class(c)) => {
3673                    let base = if c.parent.is_some() {
3674                        let pname = c
3675                            .parent
3676                            .as_ref()
3677                            .map(|p| self.callable_name(p))
3678                            .unwrap_or_default();
3679                        format!("[class {} extends {}]", c.name, pname)
3680                    } else {
3681                        format!("[class {}]", c.name)
3682                    };
3683                    self.with_callable_props(v, base, indent, st)
3684                }
3685                // A Map/Set renders its members at the NEXT nesting level, and
3686                // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
3687                // array collapses to `[Array]`. Both used to recurse through
3688                // `inspect`, which restarts at indent 0, so the depth gate never
3689                // fired: nesting printed one level too deep at every depth
3690                // (measured on node v26.7.0, four nested Maps print
3691                // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
3692                // and a SELF-referential Map or Set recursed forever and aborted
3693                // the process — `const m=new Map(); m.set('m',m); console.log(m)`
3694                // died with `fatal runtime error: stack overflow`, which no
3695                // `try`/`catch` can see. An empty one still prints in full at any
3696                // depth, as `[]`/`{}` do.
3697                // A WEAK collection never shows its contents: node prints
3698                // `WeakMap { <items unknown> }` whether it holds anything or
3699                // not, because the entries are not enumerable by design.
3700                Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
3701                Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
3702                Some(JsObj::Map { entries, .. }) => {
3703                    if entries.is_empty() {
3704                        return "Map(0) {}".into();
3705                    }
3706                    if indent > 2 * inspect_max_depth() {
3707                        return "[Map]".into();
3708                    }
3709                    let inner: Vec<String> = entries
3710                        .values()
3711                        .map(|(k, val)| {
3712                            // Sequenced, not nested in one `format!`: both arms
3713                            // need the same `&mut` cycle state.
3714                            let ks = self.inspect_lvl(k, indent + 2, st);
3715                            let vs = self.inspect_lvl(val, indent + 2, st);
3716                            format!("{ks} => {vs}")
3717                        })
3718                        .collect();
3719                    format!("Map({}) {{ {} }}", entries.len(), inner.join(", "))
3720                }
3721                Some(JsObj::Set { entries, .. }) => {
3722                    if entries.is_empty() {
3723                        return "Set(0) {}".into();
3724                    }
3725                    if indent > 2 * inspect_max_depth() {
3726                        return "[Set]".into();
3727                    }
3728                    let inner: Vec<String> = entries
3729                        .values()
3730                        .map(|v| self.inspect_lvl(v, indent + 2, st))
3731                        .collect();
3732                    format!("Set({}) {{ {} }}", entries.len(), inner.join(", "))
3733                }
3734                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
3735                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
3736                    Some(c) => match c.state {
3737                        PromiseState::Pending => "Promise { <pending> }".into(),
3738                        PromiseState::Fulfilled => {
3739                            format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
3740                        }
3741                        PromiseState::Rejected => {
3742                            format!(
3743                                "Promise {{ <rejected> {} }}",
3744                                self.inspect_lvl(&c.value, 0, st)
3745                            )
3746                        }
3747                    },
3748                    None => "Promise { <pending> }".into(),
3749                },
3750                Some(JsObj::Func(_)) => {
3751                    // `callable_name`, not the FuncDef name: an anonymous
3752                    // function expression gets its name by inference from the
3753                    // binding it initialises (`const f = function(){}`), and
3754                    // that lands as an own `name` property.
3755                    let name = self.callable_name(v);
3756                    let base = if name.is_empty() {
3757                        "[Function (anonymous)]".to_string()
3758                    } else {
3759                        format!("[Function: {name}]")
3760                    };
3761                    self.with_callable_props(v, base, indent, st)
3762                }
3763                Some(JsObj::Builtin(n)) => {
3764                    let short = n.rsplit('.').next().unwrap_or(n);
3765                    format!("[Function: {short}]")
3766                }
3767                Some(JsObj::BoundMethod { .. }) => "[Function (anonymous)]".into(),
3768                Some(JsObj::BoundFunc { target, .. }) => {
3769                    let n = self.callable_name(target);
3770                    if n.is_empty() {
3771                        "[Function: bound ]".into()
3772                    } else {
3773                        format!("[Function: bound {n}]")
3774                    }
3775                }
3776                _ => "undefined".into(),
3777            },
3778            _ => "undefined".into(),
3779        }
3780    }
3781
3782    /// Append a callable's own enumerable properties to its `[Function: f]` /
3783    /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
3784    /// A callable with none renders as the bare base.
3785    fn with_callable_props(
3786        &self,
3787        v: &Value,
3788        base: String,
3789        indent: usize,
3790        st: &mut InspectCycles,
3791    ) -> String {
3792        let mut inner: Vec<String> = self
3793            .own_enum_key_names(v)
3794            .into_iter()
3795            .map(|k| {
3796                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
3797                format!(
3798                    "{}: {}",
3799                    fmt_key(&k),
3800                    self.inspect_lvl(&val, indent + 2, st)
3801                )
3802            })
3803            .collect();
3804        for (k, val) in self.own_symbol_entries(v) {
3805            if let Some(sym) = self.symbol_of_key(&k) {
3806                inner.push(format!(
3807                    "{}: {}",
3808                    self.inspect(&sym),
3809                    self.inspect_lvl(&val, indent + 2, st)
3810                ));
3811            }
3812        }
3813        if inner.is_empty() {
3814            return base;
3815        }
3816        self.render_object(&inner, &format!("{base} "), indent)
3817    }
3818
3819    /// Render a non-empty array's already-formatted element strings, applying
3820    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
3821    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
3822    /// `values` is the raw element list (drives numeric right-alignment); `indent`
3823    /// is the array's own indentation level.
3824    fn render_array(
3825        &self,
3826        output: &[String],
3827        values: &[Value],
3828        indent: usize,
3829        has_props: bool,
3830        // `output`'s last entry is the `... N more items` tail rather than a
3831        // real element, so the grid must not size a column to it.
3832        has_tail: bool,
3833        // A constructor tag printed before the brackets, with a trailing space
3834        // (`"Uint8Array(3) "`), or empty for a plain array.
3835        base: &str,
3836    ) -> String {
3837        // Group array elements together if the array has more than six entries.
3838        // Arrays carrying extra own props (`index`/`input`/… on a match result)
3839        // are never grid-grouped — Node lays those out plainly.
3840        let entries = output.len();
3841        let (lines, grouped) = if entries > 6 && !has_props {
3842            group_array_elements(self, output, values, indent, has_tail)
3843        } else {
3844            (output.to_vec(), false)
3845        };
3846        // A typed array prints its constructor and length ahead of the brackets
3847        // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
3848        // break-length seed, so a long tag wraps the list one entry sooner.
3849        if output.is_empty() {
3850            return format!("{base}[]");
3851        }
3852        // If no grouping happened, try to line everything up on a single line.
3853        if !grouped {
3854            // start = output.length + indentationLvl + braces[0].len(1) + base + 10
3855            let start = output.len() + indent + 1 + base.chars().count() + 10;
3856            if is_below_break_length(output, start) {
3857                return format!("{base}[ {} ]", output.join(", "));
3858            }
3859        }
3860        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
3861        let pad = " ".repeat(indent);
3862        let sep = format!(",\n{pad}  ");
3863        format!("{base}[\n{pad}  {}\n{pad}]", lines.join(&sep))
3864    }
3865
3866    /// Render a non-empty object's already-formatted `key: value` strings with
3867    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
3868    /// else one property per line indented by `indent + 2`. `prefix` is the
3869    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
3870    /// Mirrors `render_array`'s break decision. (Node's `compact` depth gate is a
3871    /// no-op at `console.log`'s default depth of 2, so only length matters here.)
3872    fn render_object(&self, output: &[String], prefix: &str, indent: usize) -> String {
3873        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
3874        // For a tagged object Node folds the tag into `braces[0]` (e.g.
3875        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
3876        // prefix (which carries the trailing space) plus the `{`.
3877        let braces0 = prefix.chars().count() + 1;
3878        let start = output.len() + indent + braces0 + 10;
3879        if is_below_break_length(output, start) {
3880            return format!("{prefix}{{ {} }}", output.join(", "));
3881        }
3882        let pad = " ".repeat(indent);
3883        let sep = format!(",\n{pad}  ");
3884        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
3885    }
3886
3887    /// The `.name` of any callable (function/class/builtin/bound).
3888    pub fn callable_name(&self, v: &Value) -> String {
3889        // A user-set `.name` own property wins.
3890        if let Some(n) = self.fn_prop(v, "name") {
3891            return self.str_of(&n);
3892        }
3893        match self.get(v) {
3894            Some(JsObj::Func(f)) => self
3895                .funcs
3896                .get(f.def_id)
3897                .map(|d| d.name.clone())
3898                .unwrap_or_default(),
3899            Some(JsObj::Class(c)) => c.name.clone(),
3900            Some(JsObj::Builtin(n)) => n.rsplit('.').next().unwrap_or(n).to_string(),
3901            Some(JsObj::BoundFunc { target, .. }) => {
3902                format!("bound {}", self.callable_name(target))
3903            }
3904            _ => String::new(),
3905        }
3906    }
3907
3908    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
3909
3910    /// Strict equality (`===`): same type and same value, no coercion.
3911    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
3912        match (a, b) {
3913            (Value::Undef, Value::Undef) => true,
3914            (Value::Bool(x), Value::Bool(y)) => x == y,
3915            (Value::Str(x), Value::Str(y)) => x == y,
3916            _ => {
3917                // Numbers (NaN !== NaN, +0 === -0).
3918                let an = matches!(a, Value::Int(_) | Value::Float(_));
3919                let bn = matches!(b, Value::Int(_) | Value::Float(_));
3920                if an && bn {
3921                    let x = self.to_number(a);
3922                    let y = self.to_number(b);
3923                    return x == y;
3924                }
3925                // BigInt === BigInt compares by value (each literal is a distinct
3926                // heap cell, so reference identity would be wrong). BigInt is never
3927                // `===` a Number (different types).
3928                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
3929                    return x == y;
3930                }
3931                // Heap values.
3932                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
3933                    return sa == sb;
3934                }
3935                let na = self.is_null(a);
3936                let nb = self.is_null(b);
3937                if na || nb {
3938                    return na && nb;
3939                }
3940                // A builtin namespace/constructor/prototype is a SINGLETON in JS
3941                // (`Math === Math`, `Array.prototype === Array.prototype`), but
3942                // every bare reference here allocates a fresh handle, so compare
3943                // those by name rather than by heap index.
3944                if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
3945                    (self.get(a), self.get(b))
3946                {
3947                    return x == y;
3948                }
3949                // Reference identity for arrays/objects/functions.
3950                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
3951            }
3952        }
3953    }
3954
3955    /// Whether `v` is `null` or `undefined`.
3956    pub fn is_nullish(&self, v: &Value) -> bool {
3957        matches!(v, Value::Undef) || self.is_null(v)
3958    }
3959
3960    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
3961    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
3962    /// `"null"`, or `"object"` (array / plain object / function).
3963    fn js_type(&self, v: &Value) -> &'static str {
3964        match v {
3965            Value::Undef => "undefined",
3966            Value::Bool(_) => "boolean",
3967            Value::Int(_) | Value::Float(_) => "number",
3968            Value::Str(_) => "string",
3969            Value::Obj(_) => match self.get(v) {
3970                Some(JsObj::Str(_)) => "string",
3971                Some(JsObj::Null) => "null",
3972                Some(JsObj::BigInt(_)) => "bigint",
3973                _ => "object",
3974            },
3975            _ => "object",
3976        }
3977    }
3978
3979    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
3980    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
3981    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
3982    /// `[0] == ""` is `false` — never a number coercion of the object.
3983    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
3984        // Same type: identical to `===` (number==number, string==string, etc.).
3985        if self.strict_eq(a, b) {
3986            return true;
3987        }
3988        let ta = self.js_type(a);
3989        let tb = self.js_type(b);
3990        // null and undefined are loosely equal only to each other.
3991        if self.is_nullish(a) || self.is_nullish(b) {
3992            return self.is_nullish(a) && self.is_nullish(b);
3993        }
3994        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
3995        // values (both-BigInt was already settled by the `strict_eq` above).
3996        if ta == "bigint" || tb == "bigint" {
3997            return self.bigint_loose_eq(a, b);
3998        }
3999        if ta == tb {
4000            // Same type but not strict-equal (and not nullish) ⇒ not equal.
4001            return false;
4002        }
4003        // number ⇄ string: compare as numbers.
4004        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
4005            return self.to_number(a) == self.to_number(b);
4006        }
4007        // boolean side coerces to number, then recompares.
4008        if ta == "boolean" {
4009            return self.loose_eq(&Value::Float(self.to_number(a)), b);
4010        }
4011        if tb == "boolean" {
4012            return self.loose_eq(a, &Value::Float(self.to_number(b)));
4013        }
4014        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
4015        // then recompare as string==string or number==string.
4016        if ta == "object" && (tb == "number" || tb == "string") {
4017            let pa = self.str_of(a);
4018            return if tb == "string" {
4019                pa == self.str_of(b)
4020            } else {
4021                str_to_number(&pa) == self.to_number(b)
4022            };
4023        }
4024        if tb == "object" && (ta == "number" || ta == "string") {
4025            let pb = self.str_of(b);
4026            return if ta == "string" {
4027                self.str_of(a) == pb
4028            } else {
4029                self.to_number(a) == str_to_number(&pb)
4030            };
4031        }
4032        false
4033    }
4034
4035    /// The numeric-hook arithmetic/relational fallback for non-native operands
4036    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
4037    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
4038        use NumOp::*;
4039        match op {
4040            Add => {
4041                // `+`: if either operand is a string, concatenate string forms;
4042                // otherwise numeric addition.
4043                let a_str = self.prefers_string(a);
4044                let b_str = self.prefers_string(b);
4045                if a_str || b_str {
4046                    // String concatenation wins even with a bigint operand
4047                    // (`1n + "x"` → `"1x"`).
4048                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
4049                    Ok(self.new_str(s))
4050                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
4051                    self.bigint_arith(op, a, b)
4052                } else {
4053                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
4054                }
4055            }
4056            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
4057                self.bigint_arith(op, a, b)
4058            }
4059            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
4060            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
4061            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
4062            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
4063            Pow => Ok(Value::Float(crate::builtins::js_pow(
4064                self.to_number(a),
4065                self.to_number(b),
4066            ))),
4067            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
4068            Neg => Ok(Value::Float(-self.to_number(a))),
4069            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
4070            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
4071            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
4072        }
4073    }
4074
4075    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
4076    /// which drives `+` toward concatenation. Primitive strings qualify, and so
4077    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
4078    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
4079    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
4080    fn prefers_string(&self, v: &Value) -> bool {
4081        match v {
4082            Value::Str(_) => true,
4083            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
4084            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
4085            // string primitive either.
4086            Value::Obj(_) => !matches!(
4087                self.get(v),
4088                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
4089            ),
4090            _ => false,
4091        }
4092    }
4093
4094    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
4095    /// lexicographic, otherwise numeric (NaN yields false).
4096    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
4097        use std::cmp::Ordering;
4098        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
4099            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
4100            x.cmp(&y)
4101        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
4102            // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
4103            // order once an astral character meets a BMP one — see `utf16`.
4104            crate::utf16::cmp_units(&x, &y)
4105        } else {
4106            let x = self.to_number(a);
4107            let y = self.to_number(b);
4108            match x.partial_cmp(&y) {
4109                Some(o) => o,
4110                None => return false, // NaN operand
4111            }
4112        };
4113        match op {
4114            NumOp::Lt => ord == Ordering::Less,
4115            NumOp::Le => ord != Ordering::Greater,
4116            NumOp::Gt => ord == Ordering::Greater,
4117            NumOp::Ge => ord != Ordering::Less,
4118            _ => false,
4119        }
4120    }
4121
4122    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
4123    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
4124    /// BigInt with a Number throws, matching Node).
4125    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
4126        if self.is_bigint_val(a) || self.is_bigint_val(b) {
4127            return self.bigint_bitwise(tag, a, b);
4128        }
4129        let x = to_int32(self.to_number(a));
4130        let y = to_int32(self.to_number(b));
4131        let r: i64 = match tag {
4132            binop::BITAND => (x & y) as i64,
4133            binop::BITOR => (x | y) as i64,
4134            binop::BITXOR => (x ^ y) as i64,
4135            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
4136            binop::SHR => (x >> ((y as u32) & 31)) as i64,
4137            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
4138            _ => 0,
4139        };
4140        Ok(Value::Float(r as f64))
4141    }
4142
4143    // ── BigInt operations ────────────────────────────────────────────────────
4144    /// Whether `v` is a heap `BigInt`.
4145    pub fn is_bigint_val(&self, v: &Value) -> bool {
4146        matches!(self.get(v), Some(JsObj::BigInt(_)))
4147    }
4148    /// The `BigInt` value of `v` (a heap bigint), else `None`.
4149    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
4150        match self.get(v) {
4151            Some(JsObj::BigInt(b)) => Some(b.clone()),
4152            _ => None,
4153        }
4154    }
4155    /// Allocate a heap `BigInt`.
4156    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
4157        self.alloc(JsObj::BigInt(b))
4158    }
4159
4160    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
4161    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
4162    /// `TypeError` (a string operand is handled as concatenation before we get
4163    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
4164    /// exponent.
4165    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
4166        use num_traits::{Signed, Zero};
4167        use NumOp::*;
4168        if op == Neg {
4169            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
4170            return Ok(self.new_bigint(-x));
4171        }
4172        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
4173            (Some(x), Some(y)) => (x, y),
4174            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
4175            _ => {
4176                return Err(type_error(
4177                    "Cannot mix BigInt and other types, use explicit conversions",
4178                ))
4179            }
4180        };
4181        let r = match op {
4182            Add => x + y,
4183            Sub => x - y,
4184            Mul => x * y,
4185            Div => {
4186                if y.is_zero() {
4187                    return Err("RangeError: Division by zero".into());
4188                }
4189                x / y // truncates toward zero (matches JS BigInt division)
4190            }
4191            Mod => {
4192                if y.is_zero() {
4193                    return Err("RangeError: Division by zero".into());
4194                }
4195                x % y // sign follows the dividend (truncated), like JS
4196            }
4197            Pow => {
4198                if y.is_negative() {
4199                    return Err("RangeError: Exponent must be positive".into());
4200                }
4201                let exp = num_traits::ToPrimitive::to_u32(&y)
4202                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
4203                num_traits::Pow::pow(x, exp)
4204            }
4205            _ => return Err(type_error("unsupported BigInt operation")),
4206        };
4207        Ok(self.new_bigint(r))
4208    }
4209
4210    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
4211    /// be BigInt (mixing throws).
4212    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
4213        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
4214            (Some(x), Some(y)) => (x, y),
4215            _ => {
4216                return Err(type_error(
4217                    "Cannot mix BigInt and other types, use explicit conversions",
4218                ))
4219            }
4220        };
4221        let r = match tag {
4222            binop::BITAND => x & y,
4223            binop::BITOR => x | y,
4224            binop::BITXOR => x ^ y,
4225            binop::SHL => {
4226                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
4227                if n >= 0 {
4228                    x << (n as usize)
4229                } else {
4230                    x >> ((-n) as usize)
4231                }
4232            }
4233            binop::SHR => {
4234                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
4235                if n >= 0 {
4236                    x >> (n as usize)
4237                } else {
4238                    x << ((-n) as usize)
4239                }
4240            }
4241            binop::USHR => {
4242                return Err(type_error(
4243                    "BigInts have no unsigned right shift, use >> instead",
4244                ))
4245            }
4246            _ => return Err(type_error("unsupported BigInt operation")),
4247        };
4248        Ok(self.new_bigint(r))
4249    }
4250
4251    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
4252    /// being BigInt was already handled by `strict_eq`.
4253    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
4254        // Order so `big` is the BigInt side and `other` the counterpart.
4255        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
4256            (Some(x), _) => (x, b),
4257            (_, Some(y)) => (y, a),
4258            _ => return false,
4259        };
4260        match other {
4261            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
4262            Value::Int(n) => big == num_bigint::BigInt::from(*n),
4263            Value::Float(f) => {
4264                // Equal only when the float is an integer with the same value.
4265                if !f.is_finite() || f.fract() != 0.0 {
4266                    return false;
4267                }
4268                bigint_to_f64(&big) == *f
4269            }
4270            Value::Str(s) => match parse_bigint_str(s) {
4271                Some(bs) => big == bs,
4272                None => false,
4273            },
4274            Value::Obj(_) => match self.get(other) {
4275                // A heap string parses like a primitive string.
4276                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
4277                _ => {
4278                    // Other objects reduce via ToPrimitive (their string form).
4279                    let s = self.str_of(other);
4280                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
4281                }
4282            },
4283            _ => false,
4284        }
4285    }
4286}
4287
4288/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
4289/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
4290pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
4291    let t = crate::utf16::js_trim(s);
4292    if t.is_empty() {
4293        return Some(num_bigint::BigInt::from(0));
4294    }
4295    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
4296        (16, h)
4297    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
4298        (8, o)
4299    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
4300        (2, bb)
4301    } else {
4302        (10, t)
4303    };
4304    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
4305}
4306
4307/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
4308/// out-of-range magnitudes become ±Infinity, matching Node.
4309fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
4310    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
4311        if num_traits::Signed::is_negative(b) {
4312            f64::NEG_INFINITY
4313        } else {
4314            f64::INFINITY
4315        }
4316    })
4317}
4318
4319/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
4320fn js_mod(a: f64, b: f64) -> f64 {
4321    a % b
4322}
4323
4324/// Cycle bookkeeping for one `util.inspect` render.
4325///
4326/// `seen` is the chain of objects currently being rendered (an entry appearing
4327/// twice is a back-edge), and `refs` records every object a back-edge pointed
4328/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
4329/// in `[Circular *N]` / `<ref *N>`.
4330#[derive(Default)]
4331struct InspectCycles {
4332    seen: Vec<Value>,
4333    refs: Vec<Value>,
4334}
4335
4336impl InspectCycles {
4337    /// Record `v` as a cycle target (idempotent) and return its 1-based id.
4338    fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
4339        if let Some(id) = self.id_of(h, v) {
4340            return id;
4341        }
4342        self.refs.push(v.clone());
4343        self.refs.len()
4344    }
4345
4346    /// The `*N` id already assigned to `v`, if any.
4347    fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
4348        self.refs
4349            .iter()
4350            .position(|p| h.strict_eq(p, v))
4351            .map(|i| i + 1)
4352    }
4353}
4354
4355thread_local! {
4356    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
4357    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
4358    /// overrides it for one call, `console.log`/`util.format` use the default.
4359    static INSPECT_MAX_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(2) };
4360}
4361
4362/// Set the `util.inspect` depth for the next render (restore to 2 after).
4363pub fn set_inspect_max_depth(d: usize) {
4364    INSPECT_MAX_DEPTH.with(|c| c.set(d));
4365}
4366fn inspect_max_depth() -> usize {
4367    INSPECT_MAX_DEPTH.with(|c| c.get())
4368}
4369
4370/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
4371/// reinterpret as signed.
4372///
4373/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
4374/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
4375/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
4376/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
4377/// power-of-two modulus is exact for every finite double, so this is the whole
4378/// fix — and it is the form `Math.clz32` already used.
4379pub(crate) fn to_int32(f: f64) -> i32 {
4380    to_uint32(f) as i32
4381}
4382pub(crate) fn to_uint32(f: f64) -> u32 {
4383    if !f.is_finite() {
4384        return 0;
4385    }
4386    f.trunc().rem_euclid(4294967296.0) as u32
4387}
4388
4389/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
4390fn str_to_number(s: &str) -> f64 {
4391    let t = crate::utf16::js_trim(s);
4392    if t.is_empty() {
4393        return 0.0;
4394    }
4395    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
4396        return i64::from_str_radix(hex, 16)
4397            .map(|n| n as f64)
4398            .unwrap_or(f64::NAN);
4399    }
4400    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
4401        return i64::from_str_radix(oct, 8)
4402            .map(|n| n as f64)
4403            .unwrap_or(f64::NAN);
4404    }
4405    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
4406        return i64::from_str_radix(bin, 2)
4407            .map(|n| n as f64)
4408            .unwrap_or(f64::NAN);
4409    }
4410    match t {
4411        "Infinity" | "+Infinity" => f64::INFINITY,
4412        "-Infinity" => f64::NEG_INFINITY,
4413        _ => t.parse::<f64>().unwrap_or(f64::NAN),
4414    }
4415}
4416
4417/// `util.inspect` break length (the width past which entries wrap). Node's default.
4418const BREAK_LENGTH: usize = 80;
4419/// Node's default `compact` setting (the `compact * 4` column cap term).
4420const COMPACT: usize = 3;
4421/// Node's default `maxArrayLength` — how many array elements `util.inspect`
4422/// formats before collapsing the rest into `... N more items`.
4423const MAX_ARRAY_LENGTH: usize = 100;
4424
4425/// Whether `output` fits on a single line — a faithful port of Node's
4426/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
4427/// length (braces + indentation + slack).
4428fn is_below_break_length(output: &[String], start: usize) -> bool {
4429    let mut total = output.len() + start;
4430    if total + output.len() > BREAK_LENGTH {
4431        return false;
4432    }
4433    for o in output {
4434        if o.contains('\n') {
4435            return false;
4436        }
4437        total += o.chars().count();
4438        if total > BREAK_LENGTH {
4439            return false;
4440        }
4441    }
4442    true
4443}
4444
4445/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
4446/// already-formatted element strings into an aligned multi-column grid. Returns
4447/// `(lines, grouped)` — `grouped` is false when Node would leave the output
4448/// ungrouped (so the caller falls back to single-line / one-per-line).
4449fn group_array_elements(
4450    host: &JsHost,
4451    output: &[String],
4452    values: &[Value],
4453    indentation_lvl: usize,
4454    has_tail: bool,
4455) -> (Vec<String>, bool) {
4456    let separator_space = 2usize; // ", " between entries
4457                                  // A `... N more items` tail is not an element: node drops it from the grid
4458                                  // (`outputLength--`) so it neither widens a column nor occupies a cell, then
4459                                  // re-appends it as its own final line.
4460    let output_length = output.len() - usize::from(has_tail);
4461    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
4462    let mut total_length = 0usize;
4463    let mut max_length = 0usize;
4464    for &len in &data_len[..output_length] {
4465        total_length += len + separator_space;
4466        if len > max_length {
4467            max_length = len;
4468        }
4469    }
4470    let actual_max = max_length + separator_space;
4471    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
4472    if !(actual_max * 3 + indentation_lvl < BREAK_LENGTH
4473        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
4474    {
4475        return (output.to_vec(), false);
4476    }
4477    let approx_char_heights = 2.5f64;
4478    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
4479    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
4480    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
4481    let columns = [
4482        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
4483            as i64,
4484        ((BREAK_LENGTH - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
4485        (COMPACT * 4) as i64,
4486        15,
4487    ]
4488    .into_iter()
4489    .min()
4490    .unwrap();
4491    if columns <= 1 {
4492        return (output.to_vec(), false);
4493    }
4494    let columns = columns as usize;
4495    // The widest entry (plus separator) in each column.
4496    let mut max_line_length = vec![0usize; columns];
4497    for (i, slot) in max_line_length.iter_mut().enumerate() {
4498        let mut line_length = 0;
4499        let mut j = i;
4500        while j < output_length {
4501            if data_len[j] > line_length {
4502                line_length = data_len[j];
4503            }
4504            j += columns;
4505        }
4506        *slot = line_length + separator_space;
4507    }
4508    // Right-align (padStart) only when every element is a number/bigint.
4509    let pad_start = values.iter().all(|v| {
4510        matches!(v, Value::Int(_) | Value::Float(_))
4511            || matches!(host.get(v), Some(JsObj::BigInt(_)))
4512    });
4513    let mut tmp = Vec::new();
4514    let mut i = 0;
4515    while i < output_length {
4516        let max = (i + columns).min(output_length);
4517        let mut str_line = String::new();
4518        let mut j = i;
4519        while j < max.saturating_sub(1) {
4520            // `output[j]` has no colors here, so padding == max_line_length[col].
4521            let col = j - i;
4522            let cell = format!("{}, ", output[j]);
4523            let target = max_line_length[col];
4524            str_line.push_str(&pad_to(&cell, target, pad_start));
4525            j += 1;
4526        }
4527        // The last cell of the row: right-aligned entries pad without the ", ".
4528        if pad_start {
4529            let col = j - i;
4530            let target = max_line_length[col] - separator_space;
4531            str_line.push_str(&pad_to(&output[j], target, true));
4532        } else {
4533            str_line.push_str(&output[j]);
4534        }
4535        tmp.push(str_line);
4536        i += columns;
4537    }
4538    if has_tail {
4539        tmp.push(output[output_length].clone());
4540    }
4541    (tmp, true)
4542}
4543
4544/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
4545/// (Padding is measured in chars; already ANSI-free here.)
4546fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
4547    let len = s.chars().count();
4548    if len >= width {
4549        return s.to_string();
4550    }
4551    let fill = " ".repeat(width - len);
4552    if pad_start {
4553        format!("{fill}{s}")
4554    } else {
4555        format!("{s}{fill}")
4556    }
4557}
4558
4559/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
4560/// `lib/internal/util/inspect.js`.
4561///
4562/// The quote character is chosen so the contents need as little escaping as
4563/// possible: single quotes normally, double quotes when the string contains a
4564/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
4565/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
4566/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
4567/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
4568fn quote_str(s: &str) -> String {
4569    let quote = if !s.contains('\'') {
4570        '\''
4571    } else if !s.contains('"') {
4572        '"'
4573    } else if !s.contains('`') && !s.contains("${") {
4574        '`'
4575    } else {
4576        '\''
4577    };
4578    let mut out = String::with_capacity(s.len() + 2);
4579    out.push(quote);
4580    for c in s.chars() {
4581        match c {
4582            _ if c == quote => {
4583                out.push('\\');
4584                out.push(c);
4585            }
4586            '\\' => out.push_str("\\\\"),
4587            '\u{8}' => out.push_str("\\b"),
4588            '\t' => out.push_str("\\t"),
4589            '\n' => out.push_str("\\n"),
4590            '\u{c}' => out.push_str("\\f"),
4591            '\r' => out.push_str("\\r"),
4592            '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
4593            _ => out.push(c),
4594        }
4595    }
4596    out.push(quote);
4597    out
4598}
4599
4600/// Render an object key: bare if it is a valid identifier, quoted otherwise.
4601fn fmt_key(k: &str) -> String {
4602    let ok = !k.is_empty()
4603        && k.chars()
4604            .next()
4605            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
4606            .unwrap_or(false)
4607        && k.chars()
4608            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
4609    if ok {
4610        k.to_string()
4611    } else {
4612        quote_str(k)
4613    }
4614}
4615
4616// ── iteration ────────────────────────────────────────────────────────────────
4617
4618impl JsHost {
4619    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
4620    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
4621    /// holds no host borrow across resumes.
4622    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
4623        match self.get(v) {
4624            Some(JsObj::Array(items)) => Ok(items.clone()),
4625            Some(JsObj::Str(s)) => {
4626                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
4627                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
4628            }
4629            Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
4630            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
4631            Some(JsObj::Map { entries, .. }) => {
4632                // Map iterates as `[key, value]` pairs.
4633                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
4634                Ok(pairs
4635                    .into_iter()
4636                    .map(|(k, v)| self.new_array(vec![k, v]))
4637                    .collect())
4638            }
4639            // A `Buffer` iterates over its BYTES and a typed array over its
4640            // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
4641            // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
4642            // "object is not iterable", which is the same invariant holding at
4643            // one of its two sites.
4644            Some(JsObj::Object(props))
4645                if props.contains_key("@@bytes") || props.contains_key("@@elems") =>
4646            {
4647                let field = if props.contains_key("@@bytes") {
4648                    "@@bytes"
4649                } else {
4650                    "@@elems"
4651                };
4652                match props
4653                    .get(field)
4654                    .cloned()
4655                    .and_then(|b| self.get(&b).cloned())
4656                {
4657                    Some(JsObj::Array(items)) => Ok(items),
4658                    _ => Ok(Vec::new()),
4659                }
4660            }
4661            // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
4662            // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
4663            // produced `number is not iterable`, which no engine emits.
4664            _ => {
4665                let shown = self.inspect(v);
4666                Err(type_error(&format!("{shown} is not iterable")))
4667            }
4668        }
4669    }
4670
4671    /// Enumerable string keys of an object/array (for `for-in`). Internal
4672    /// symbol-keyed props (`@@…`) are not enumerable.
4673    /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
4674    /// not already seen, walking the whole prototype chain. Class methods and the
4675    /// builtin prototypes are non-enumerable, so in practice this only surfaces
4676    /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
4677    /// is exactly the constructor-function idiom older packages are written in.
4678    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
4679        let mut keys = self.own_enum_key_names(v);
4680        let mut cur = self.proto_of(v);
4681        let mut hops = 0;
4682        while let Some(p) = cur {
4683            // A cyclic or pathologically deep chain must not hang the loop.
4684            hops += 1;
4685            if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
4686                break;
4687            }
4688            for k in self.own_enum_key_names(&p) {
4689                if !keys.contains(&k) {
4690                    keys.push(k);
4691                }
4692            }
4693            cur = self.proto_of(&p);
4694        }
4695        keys.into_iter().map(|k| self.new_str(k)).collect()
4696    }
4697
4698    /// The own *enumerable* string keys of `v`, in property order — the single
4699    /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
4700    /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
4701    /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
4702    /// `prop_attrs` are excluded.
4703    pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
4704        self.own_key_names(v, true)
4705    }
4706
4707    /// Own string keys of `v` in insertion order. `enum_only` drops the
4708    /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
4709    /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
4710    pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
4711        let mut keys = self.own_enum_data_keys(v, enum_only);
4712        // An accessor defined before its object had any ordering marker (a class
4713        // prototype accessor, say) still has to appear.
4714        for k in self.own_accessor_keys(v) {
4715            if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
4716                keys.push(k);
4717            }
4718        }
4719        keys
4720    }
4721
4722    /// The keys that own a slot in the object's property map, in insertion
4723    /// order, resolving accessor ordering markers back to their real key.
4724    fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
4725        match self.get(v) {
4726            // A `Buffer` is an index-keyed exotic: its own enumerable keys are
4727            // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
4728            // the `length`/`byteLength` view metadata, which V8 keeps on the
4729            // prototype chain or as non-enumerable own slots.
4730            // A `Buffer` and every other typed array are index-keyed exotics:
4731            // their own enumerable keys are `"0".."len-1"` (the elements live in
4732            // a hidden slot), never the `length`/`byteLength` view metadata,
4733            // which V8 keeps on the prototype chain or as non-enumerable own
4734            // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
4735            // and `JSON.stringify(u8)` was `{}` where node gives
4736            // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
4737            // the two views of the same question disagreed.
4738            Some(JsObj::Object(props))
4739                if matches!(
4740                    props.get("@@native").map(|t| self.str_of(t)).as_deref(),
4741                    Some("Buffer") | Some("TypedArray")
4742                ) =>
4743            {
4744                let field = match props.get("@@native").map(|t| self.str_of(t)).as_deref() {
4745                    Some("Buffer") => "@@bytes",
4746                    _ => "@@elems",
4747                };
4748                let n = match props.get(field).and_then(|b| self.get(b)) {
4749                    Some(JsObj::Array(items)) => items.len(),
4750                    _ => 0,
4751                };
4752                (0..n).map(|i| i.to_string()).collect()
4753            }
4754            Some(JsObj::Object(props)) => props
4755                .keys()
4756                .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
4757                    Some(real) => Some(real.to_string()),
4758                    None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
4759                    None => None,
4760                })
4761                .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
4762                .collect(),
4763            // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
4764            // ascending, then the exotic non-enumerable `length`, then the
4765            // ordinary string keys in insertion order. Those ordinary keys have
4766            // no property map to live in — a `str.match()` result's
4767            // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
4768            // in the fn-prop side table — so they are read back from there.
4769            Some(JsObj::Array(items)) => {
4770                // An ELIDED element is not an own property at all, so it
4771                // contributes no key — the difference behind
4772                // `Object.keys([1,,3])` being `['0','2']`.
4773                let mut keys: Vec<String> = (0..items.len())
4774                    .filter(|i| !self.is_hole(v, *i))
4775                    .map(|i| i.to_string())
4776                    .collect();
4777                if !enum_only {
4778                    keys.push("length".into());
4779                }
4780                keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
4781                    !k.starts_with("@@")
4782                        && !k.starts_with('#')
4783                        && (!enum_only || self.prop_attrs(v, k).enumerable)
4784                }));
4785                keys
4786            }
4787            // A function/class keeps every own property in the side table. Its
4788            // exotic `name`/`length`/`prototype` and its class methods are all
4789            // non-enumerable, so under `enum_only` what is left is exactly what
4790            // a script assigned; `getOwnPropertyNames` reports the exotics too,
4791            // in V8's order (`length`, `name`, `prototype`, then the rest).
4792            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
4793                let mut keys: Vec<String> = Vec::new();
4794                if !enum_only {
4795                    keys.push("length".into());
4796                    keys.push("name".into());
4797                    if self.owns_prototype(v) {
4798                        keys.push("prototype".into());
4799                    }
4800                }
4801                let rest: Vec<String> = self
4802                    .fn_prop_keys(v)
4803                    .into_iter()
4804                    // An accessor's ordering marker resolves back to its real
4805                    // key, so a static getter enumerates where it was declared.
4806                    .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
4807                        Some(real) => Some(real.to_string()),
4808                        None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
4809                        None => None,
4810                    })
4811                    .filter(|k| {
4812                        !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
4813                    })
4814                    .collect();
4815                keys.extend(rest);
4816                keys
4817            }
4818            // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
4819            // members node-js implements, so a package that copies a namespace
4820            // key-by-key gets the working set instead of an empty object.
4821            Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
4822            _ => Vec::new(),
4823        }
4824    }
4825
4826    /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
4827    /// through the byte store; everything else reads the property map. Own
4828    /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
4829    /// their getters, which cannot happen under the host borrow.
4830    pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
4831        self.own_enum_key_names(v)
4832            .into_iter()
4833            .map(|k| {
4834                let val = match self.get(v) {
4835                    // A Buffer's index keys read out of the hidden `@@bytes`
4836                    // array; resolve inline rather than through
4837                    // `buffer::byte_get`, which would re-borrow the host.
4838                    Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
4839                        // A Buffer's elements live in `@@bytes` and every
4840                        // other typed array's in `@@elems`; both are index
4841                        // keys with no entry in the property map.
4842                        let backing = props
4843                            .get("@@bytes")
4844                            .or_else(|| props.get("@@elems"))
4845                            .and_then(|b| self.get(b));
4846                        match (backing, k.parse::<usize>()) {
4847                            (Some(JsObj::Array(items)), Ok(i)) => {
4848                                items.get(i).cloned().unwrap_or(Value::Undef)
4849                            }
4850                            _ => Value::Undef,
4851                        }
4852                    }),
4853                    // An index reads the element; any other own key (`foo`,
4854                    // a match result's `index`) lives in the side table.
4855                    Some(JsObj::Array(items)) => k
4856                        .parse::<usize>()
4857                        .ok()
4858                        .and_then(|i| items.get(i).cloned())
4859                        .or_else(|| self.fn_prop(v, &k))
4860                        .unwrap_or(Value::Undef),
4861                    Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
4862                        self.fn_prop(v, &k).unwrap_or(Value::Undef)
4863                    }
4864                    _ => Value::Undef,
4865                };
4866                (k, val)
4867            })
4868            .collect()
4869    }
4870}
4871
4872/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
4873/// accessor's getter invoked — the observable shape `Object.values`,
4874/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
4875/// outside a `with_host` borrow because a getter re-enters the host.
4876pub fn own_enum_entries_deep(v: &Value) -> Vec<(String, Value)> {
4877    // A Proxy has no property map at all: its own enumerable entries come from
4878    // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
4879    // throws surfaces as an empty result here because this signature is
4880    // infallible; the callers that MUST propagate a trap throw (`Object.keys`
4881    // and friends) go through `builtins::object_keys`, which does.
4882    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
4883        return crate::proxy::own_enum_entries(v).unwrap_or_default();
4884    }
4885    // A builtin namespace (`require('path')`, `Buffer`) has no property map at
4886    // all: its members are resolved on demand by `namespace_property`, which
4887    // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
4888    // Without this, spread and `Object.assign` copied the namespace's KEYS with
4889    // `undefined` for every value — measured against node v26.7.0,
4890    // `{...require('path')}.join` was `undefined` here and a function there,
4891    // while `Object.entries(require('path'))` (which resolves through
4892    // `builtins`, not through this borrow) was already correct. Two enumeration
4893    // paths, one of them silently value-less.
4894    if let Some(ns) = with_host(|h| match h.get(v) {
4895        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
4896        _ => None,
4897    }) {
4898        return with_host(|h| h.own_enum_key_names(v))
4899            .into_iter()
4900            .map(|k| {
4901                let val = crate::builtins::namespace_property(&ns, &k);
4902                (k, val)
4903            })
4904            .collect();
4905    }
4906    let accessor_keys: Vec<String> = with_host(|h| {
4907        h.own_accessor_keys(v)
4908            .into_iter()
4909            .filter(|k| h.prop_attrs(v, k).enumerable)
4910            .collect()
4911    });
4912    let entries = with_host(|h| h.own_enum_entries(v));
4913    entries
4914        .into_iter()
4915        .map(|(k, val)| {
4916            if accessor_keys.contains(&k) {
4917                let got = get_prop_chain(v, &k).unwrap_or(Value::Undef);
4918                (k, got)
4919            } else {
4920                (k, val)
4921            }
4922        })
4923        .collect()
4924}
4925
4926// ── function invocation ──────────────────────────────────────────────────────
4927
4928/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
4929/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
4930/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
4931/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
4932/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
4933/// Float→i64/f64 per the export signature).
4934fn marshal_ffi_arg(v: &Value) -> Value {
4935    match v {
4936        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
4937            Some(s) => Value::str(s),
4938            None => v.clone(),
4939        },
4940        _ => v.clone(),
4941    }
4942}
4943
4944/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
4945pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
4946    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
4947    // line)`; compile + register the block's exported functions, returning JS
4948    // `undefined` (`Value::Undef`).
4949    if name == "__rust_compile" {
4950        let b64 = args
4951            .first()
4952            .map(|v| with_host(|h| h.str_of(v)))
4953            .unwrap_or_default();
4954        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
4955    }
4956    if let Some(v) = with_host(|h| h.read_name(name)) {
4957        return invoke(&v, args, None);
4958    }
4959    // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
4960    // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
4961    // and `[eval][0](src)` all reach the same function value but are INDIRECT
4962    // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
4963    // This is the one place the two forms are distinguishable without a compiler
4964    // change: `call_named` is reached only from `ops::CALL`, which the compiler
4965    // emits exclusively for a bare-identifier callee, while every value-call form
4966    // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
4967    // has already established that `eval` is not shadowed by a user binding.
4968    if name == "eval" {
4969        return crate::builtins::eval_source(args.first(), true);
4970    }
4971    if crate::builtins::is_known_builtin(name) {
4972        return crate::builtins::call_builtin_function(name, args);
4973    }
4974    // A `rust { ... }` block's exported functions are callable by bareword.
4975    // Reached only after user names/globals and builtins all miss, so JS code
4976    // always wins; the registry membership check keeps this off the hot path.
4977    if fusevm::ffi::is_registered(name) {
4978        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
4979        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
4980            return r;
4981        }
4982    }
4983    Err(ref_error(name))
4984}
4985
4986/// `recv.name(args)`.
4987pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
4988    // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
4989    // comes first: an unbranded receiver throws here rather than reporting the
4990    // method missing. Only a `#`-prefixed name pays the extra probe.
4991    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
4992        return Err(crate::builtins::private_brand_message(name, false));
4993    }
4994    // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
4995    // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
4996    // below all read a property map a proxy does not have.
4997    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4998        let f = crate::builtins::get_property(recv, name)?;
4999        if !with_host(|h| is_callable(h, &f)) {
5000            return Err(type_error(&format!("{name} is not a function")));
5001        }
5002        // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
5003        // `Object.prototype` methods are generic over `this`. node-js models each
5004        // as a thunk BOUND to the object it was read off — through a proxy, that
5005        // is the target — so invoking the thunk answers for the target and skips
5006        // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
5007        // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
5008        // those against the PROXY, which is the `this` the real method receives.
5009        //
5010        // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
5011        // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
5012        // an array stringifies `1,2` through `Array.prototype.toString`, not
5013        // `[object Object]`), which the bound thunk already gets right.
5014        if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
5015            if with_host(|h| is_callable(h, recv)) {
5016                if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
5017                    return Ok(r);
5018                }
5019            }
5020            if matches!(
5021                name,
5022                "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
5023            ) {
5024                return crate::builtins::object_builtin_method(recv, name, args);
5025            }
5026        }
5027        return invoke(&f, args, Some(recv.clone()));
5028    }
5029    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
5030    // name.
5031    if let Some(ns) = with_host(|h| match h.get(recv) {
5032        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
5033        _ => None,
5034    }) {
5035        let qualified = format!("{ns}.{name}");
5036        if crate::builtins::is_known_builtin(&qualified) {
5037            return crate::builtins::call_builtin_function(&qualified, args);
5038        }
5039    }
5040    // Object / instance: an accessor getter that yields a function, an own or
5041    // inherited method (class methods live on the prototype chain), then an
5042    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
5043    // directly — NOT get_property — so the Object.prototype-builtin fallback
5044    // never routes back through a BoundMethod and recurses.
5045    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
5046        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
5047        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
5048        // A user-added or reparented-prototype method takes precedence over the
5049        // native dispatcher — matching JS resolution order (own → prototype
5050        // chain). This is what lets Express work: it does
5051        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
5052        // where `send` is a plain function on the reparented prototype. Native
5053        // instance methods (`res.end`/`write`/…) are NOT stored as plain
5054        // function properties, so `lookup_chain` misses them and we fall through
5055        // to `instance_call` for the real native behavior.
5056        if let Some(tag) = crate::stdlib::native_tag(recv) {
5057            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
5058                if with_host(|h| is_callable(h, &f)) {
5059                    return invoke(&f, args, Some(recv.clone()));
5060                }
5061            }
5062            // `Object.prototype` methods reach a native instance too — a Buffer
5063            // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
5064            // chain, and the native dispatcher has no entry for them.
5065            if crate::builtins::is_object_builtin_method(name)
5066                && !crate::stdlib::instance_has_method(&tag, name)
5067            {
5068                return crate::builtins::object_builtin_method(recv, name, args);
5069            }
5070            return crate::stdlib::instance_call(&tag, recv, name, args);
5071        }
5072        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
5073            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
5074            if with_host(|h| is_callable(h, &f)) {
5075                return invoke(&f, args, Some(recv.clone()));
5076            }
5077        }
5078        // A Proxy in the prototype chain serves the method through its `get`
5079        // trap. `lookup_chain` below reads property maps, which a proxy has none
5080        // of, so without this `child.m()` on `Object.create(proxy)` reported
5081        // "m is not a function" even though `child.m` already read correctly.
5082        if crate::builtins::proxy_proto_link(recv, name).is_some() {
5083            let f = crate::builtins::get_property(recv, name)?;
5084            if !with_host(|h| is_callable(h, &f)) {
5085                return Err(type_error(&format!("{name} is not a function")));
5086            }
5087            return invoke(&f, args, Some(recv.clone()));
5088        }
5089        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
5090            if with_host(|h| is_callable(h, &f)) {
5091                return invoke(&f, args, Some(recv.clone()));
5092            }
5093            return Err(type_error(&format!("{name} is not a function")));
5094        }
5095        if crate::builtins::is_object_builtin_method(name) {
5096            return crate::builtins::object_builtin_method(recv, name, args);
5097        }
5098        if name == "constructor" {
5099            if let Some(r) = call_default_ctor(recv, &args) {
5100                return r;
5101            }
5102        }
5103        return Err(type_error(&format!("{name} is not a function")));
5104    }
5105    // Function value methods: call / apply / bind, then any static method stored
5106    // on the function object.
5107    if matches!(
5108        with_host(|h| h.kind_of(recv)),
5109        Some(ObjKind::Func)
5110            | Some(ObjKind::Class)
5111            | Some(ObjKind::BoundFunc)
5112            | Some(ObjKind::BoundMethod)
5113            | Some(ObjKind::Builtin)
5114    ) {
5115        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
5116            return Ok(r);
5117        }
5118        // A static method (own or inherited): `this` is the constructor (`recv`).
5119        let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
5120            with_host(|h| h.class_static(recv, name))
5121        } else {
5122            with_host(|h| h.fn_prop(recv, name))
5123        };
5124        if let Some(f) = stat {
5125            if with_host(|h| is_callable(h, &f)) {
5126                return invoke(&f, args, Some(recv.clone()));
5127            }
5128        }
5129        // `class_static` only walks user-class `extends` links, so a chain that
5130        // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
5131        // could not reach that builtin's statics: `D.from([1,2])` threw
5132        // "from is not a function" even though `typeof D.from` said `function`.
5133        // Re-dispatch the call against that ancestor, which is what reaches a
5134        // builtin namespace's methods.
5135        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
5136            if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
5137                if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
5138                    return call_method(&anc, name, args);
5139                }
5140            }
5141        }
5142        // A method inherited via the function's [[Prototype]] chain (set with
5143        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
5144        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
5145        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
5146            if with_host(|h| is_callable(h, &f)) {
5147                return invoke(&f, args, Some(recv.clone()));
5148            }
5149        }
5150        // An `Object.prototype` method invoked with a builtin namespace/prototype
5151        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
5152        // ownership probe) — dispatch it against the builtin receiver.
5153        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
5154            && crate::builtins::is_object_builtin_method(name)
5155        {
5156            return crate::builtins::object_builtin_method(recv, name, args);
5157        }
5158    }
5159    if name == "constructor" {
5160        if let Some(r) = call_default_ctor(recv, &args) {
5161            return r;
5162        }
5163    }
5164    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
5165    crate::builtins::call_type_method(recv, name, args)
5166}
5167
5168/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
5169/// owns a `constructor` slot.
5170///
5171/// Reading the property already resolves a builtin instance's native constructor
5172/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
5173/// consulted the prototype chain, so the two disagreed:
5174/// `(function(){}).constructor === Function` read `true` while
5175/// `(function(){}).constructor('return 9')` threw
5176/// `TypeError: constructor is not a function`. That call form is exactly how
5177/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
5178/// constructor. Resolved here through the same one definition the read uses, so
5179/// the two can no longer drift apart. `None` means "not resolvable/callable",
5180/// leaving the caller's original error in place.
5181fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
5182    let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
5183    with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
5184}
5185
5186/// Call any callable value.
5187pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
5188    // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
5189    // Probed by kind first so the ordinary call path never clones its arguments.
5190    if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
5191        return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
5192    }
5193    let obj = with_host(|h| h.get(callable).cloned());
5194    match obj {
5195        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
5196        // against the invoke-time `this` (supplied by `.call`/`.apply`).
5197        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
5198            let recv = this.unwrap_or(Value::Undef);
5199            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
5200        }
5201        // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
5202        // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
5203        // this:
5204        //
5205        //     function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
5206        //     InternalDecoder.prototype = StringDecoder.prototype;
5207        //
5208        // A native constructor builds a fresh tagged object, so initializing the
5209        // SUPPLIED object means building one and moving its slots across.
5210        //
5211        // The guard is deliberately narrow: `obj` must already inherit from THIS
5212        // constructor's prototype, i.e. the subclass really did adopt it. Without
5213        // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
5214        // return a string / a buffer — would start mutating `x` instead.
5215        Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
5216            let target = this.expect("guard checked");
5217            let built = crate::stdlib::construct(name, &args)
5218                .expect("guard checked a native constructor")?;
5219            adopt_native_slots(&target, &built);
5220            Ok(Value::Undef)
5221        }
5222        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
5223        Some(JsObj::Func(fv)) => run_user_func(&fv, args, this),
5224        // A method read off an object is modelled as a thunk BOUND to it, but an
5225        // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
5226        // rebinds `this`, and every `Array.prototype` method is generic over it, so
5227        // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
5228        // override made that read back as the empty array the thunk was read off.
5229        // A nullish override is ignored: it carries no receiver to dispatch on.
5230        Some(JsObj::BoundMethod { recv, name }) => {
5231            let target = match &this {
5232                Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
5233                _ => &recv,
5234            };
5235            // A thunk read off an ARRAY carries an `Array.prototype` method, and
5236            // those are generic over `this` — route the rebound call through
5237            // `proto_method` so an array-LIKE receiver takes the generic path
5238            // instead of being told the method does not exist.
5239            if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
5240                return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
5241            }
5242            call_method(target, &name, args)
5243        }
5244        Some(JsObj::BoundFunc {
5245            target,
5246            this: bthis,
5247            args: pre,
5248        }) => {
5249            let mut all = pre;
5250            all.extend(args);
5251            invoke(&target, all, Some(bthis))
5252        }
5253        Some(JsObj::Class(c)) => Err(type_error(&format!(
5254            "Class constructor {} cannot be invoked without 'new'",
5255            c.name
5256        ))),
5257        _ => Err(type_error(&format!(
5258            "{} is not a function",
5259            with_host(|h| h.str_of(callable))
5260        ))),
5261    }
5262}
5263
5264/// Whether calling the native constructor `name` with `this` is the ES5
5265/// constructor-stealing pattern rather than an ordinary call.
5266///
5267/// True only when `name` really is a native stdlib constructor AND `this` is a
5268/// plain object that already inherits from that constructor's prototype — the
5269/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
5270/// object that merely happens to be passed as `this` does not qualify, so
5271/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
5272fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
5273    let Some(target) = this else { return false };
5274    if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
5275        return false;
5276    }
5277    // Already initialized (e.g. a re-entrant call) — nothing to steal.
5278    if crate::stdlib::native_tag(target).is_some() {
5279        return false;
5280    }
5281    let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
5282        return false;
5283    };
5284    let mut cur = with_host(|h| h.proto_of(target));
5285    while let Some(p) = cur {
5286        if p == proto {
5287            return true;
5288        }
5289        cur = with_host(|h| h.proto_of(&p));
5290    }
5291    false
5292}
5293
5294/// Move a freshly-constructed native instance's state onto `target`, so an
5295/// object built by a subclass constructor becomes a working instance of the
5296/// native class. Copies every own key the native constructor set — the hidden
5297/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
5298/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
5299fn adopt_native_slots(target: &Value, built: &Value) {
5300    let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
5301        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
5302        _ => Vec::new(),
5303    });
5304    with_host(|h| {
5305        if let Some(JsObj::Object(p)) = h.get_mut(target) {
5306            for (k, v) in slots {
5307                p.insert(k, v);
5308            }
5309        }
5310    });
5311}
5312
5313/// Execute a user function/closure body on a fresh frame.
5314pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
5315    run_user_func_nt(fv, args, this, None)
5316}
5317
5318/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
5319pub fn run_user_func_nt(
5320    fv: &FuncVal,
5321    args: Vec<Value>,
5322    this: Option<Value>,
5323    new_target: Option<Value>,
5324) -> Result<Value, String> {
5325    // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
5326    // the entire compiled body, `sub_chunks` and all — on every single call.
5327    // The chunk is now reached once per pooled VM, in the two arms below.
5328    let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
5329        let d = &h.funcs[fv.def_id];
5330        (
5331            d.params.clone(),
5332            d.is_generator,
5333            d.is_async,
5334            d.is_arrow,
5335            d.name.clone(),
5336        )
5337    });
5338    let env = new_env(fv.env.clone());
5339    // Bind the simple/rest arg slots; destructuring + defaults run in the body
5340    // prologue (compiled ahead of the user statements).
5341    bind_params(&env, &params, args, is_arrow_def);
5342    // Arrow functions capture `this` lexically; regular functions receive it.
5343    let this_val = if fv.is_arrow { fv.this.clone() } else { this };
5344    // A generator function does not run its body on call — it returns a suspended
5345    // generator over the already-bound frame.
5346    if is_generator {
5347        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
5348        let gen = make_generator(chunk, env, this_val, fv.home_class.clone());
5349        if is_async {
5350            if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
5351                with_host(|h| h.generators[id as usize].async_gen = true);
5352            }
5353        }
5354        return Ok(gen);
5355    }
5356    // An async function runs on a coroutine and returns a Promise: it executes
5357    // synchronously up to the first `await`, then continues via microtasks.
5358    if is_async {
5359        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
5360        let gen = make_generator(chunk, env, this_val, fv.home_class.clone());
5361        return Ok(run_async(gen));
5362    }
5363    let home = fv
5364        .home_class
5365        .as_ref()
5366        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
5367    with_host(|h| {
5368        h.frames.push(Frame {
5369            base_env: env.clone(),
5370            env,
5371            this_obj: this_val,
5372            new_target,
5373            home_class: home,
5374            line: 0,
5375            owner: Some(def_name),
5376            is_module: false,
5377        })
5378    });
5379    let r = run_chunk_keyed(func_key(fv.def_id), || {
5380        with_host(|h| h.funcs[fv.def_id].chunk.clone())
5381    });
5382    let sig = with_host(|h| {
5383        h.frames.pop();
5384        h.signal.take()
5385    });
5386    match r {
5387        Err(e) => Err(e),
5388        Ok(_) => Ok(match sig {
5389            Some(Signal::Return(v)) => v,
5390            _ => Value::Undef,
5391        }),
5392    }
5393}
5394
5395/// Bind positional args into a fresh call environment. The compiler emits the
5396/// param names in `def.params`; a `...rest` slot collects the tail as an array.
5397fn bind_params(env: &Env, params: &[ParamSlot], args: Vec<Value>, is_arrow: bool) {
5398    let mut vars = VarMap::default();
5399    let mut i = 0;
5400    for slot in params {
5401        if slot.rest {
5402            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
5403            let arr = with_host(|h| h.new_array(rest));
5404            vars.insert(slot.name.clone(), arr);
5405        } else {
5406            let v = args.get(i).cloned().unwrap_or(Value::Undef);
5407            vars.insert(slot.name.clone(), v);
5408            i += 1;
5409        }
5410    }
5411    // `arguments` array (simple approximation — see BUGS.md: it is a real
5412    // Array, not an Arguments exotic). An ARROW function never gets one:
5413    // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
5414    // a non-arrow, so `arguments` inside an arrow resolves lexically to the
5415    // enclosing function's. Binding an empty one here made
5416    // `function f(){ const g = () => [...arguments]; }` see zero args.
5417    if !is_arrow {
5418        let args_arr = with_host(|h| h.new_array(args));
5419        vars.entry("arguments".to_string()).or_insert(args_arr);
5420    }
5421    env.borrow_mut().vars = vars;
5422}
5423
5424/// Construct an instance with `new` — creates a fresh object, binds it as
5425/// `this`, runs the constructor, and returns the object (unless the constructor
5426/// returns its own object).
5427pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
5428    construct_nt(ctor, args, ctor.clone())
5429}
5430
5431/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
5432/// calls `super(...)` — the target stays the originally-`new`ed class).
5433pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
5434    // `new proxy(…)` runs the `construct` trap (or forwards to the target).
5435    if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
5436        return crate::proxy::construct(ctor, args, &new_target)
5437            .map(|r| r.expect("kind_of said Proxy"));
5438    }
5439    let obj = with_host(|h| h.get(ctor).cloned());
5440    match obj {
5441        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
5442        Some(JsObj::Func(fv)) => {
5443            // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
5444            // `function*` and an `async function` are callable but not
5445            // constructable (10.2.2 is installed only for the ordinary case), so
5446            // `new` on one is a TypeError — node-js instead ran the body and
5447            // handed back a half-built instance (for a generator, an object whose
5448            // constructor had returned a suspended generator).
5449            let non_ctor = with_host(|h| {
5450                h.funcs
5451                    .get(fv.def_id)
5452                    // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
5453                    // is `TypeError: o.m is not a constructor` on node v26.7.0,
5454                    // which is also why a method owns no `prototype`.
5455                    .map(|d| d.is_generator || d.is_async || d.is_method)
5456                    .unwrap_or(false)
5457            });
5458            if fv.is_arrow || non_ctor {
5459                return Err(not_a_constructor(ctor));
5460            }
5461            // A plain constructor function: instance delegates to `fn.prototype`
5462            // (auto-created with a `.constructor` back-link if not yet accessed).
5463            let inst = with_host(|h| {
5464                let o = h.new_object(IndexMap::new());
5465                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
5466                    let p = h.new_object(IndexMap::new());
5467                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
5468                        pp.insert("constructor".to_string(), ctor.clone());
5469                    }
5470                    // `F.prototype.constructor` is non-enumerable in JS.
5471                    h.hide_prop(&p, "constructor");
5472                    h.set_fn_prop(ctor, "prototype", p.clone());
5473                    p
5474                });
5475                h.set_proto(&o, proto);
5476                o
5477            });
5478            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
5479            if returns_object(&r) {
5480                Ok(r)
5481            } else {
5482                Ok(inst)
5483            }
5484        }
5485        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
5486        Some(JsObj::BoundFunc {
5487            target, args: pre, ..
5488        }) => {
5489            let mut all = pre;
5490            all.extend(args);
5491            construct_nt(&target, all, new_target)
5492        }
5493        _ => Err(not_a_constructor(ctor)),
5494    }
5495}
5496
5497/// `TypeError: <callee> is not a constructor`.
5498///
5499/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
5500/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
5501/// its name — the same string in the common case — and anything else by its
5502/// value.
5503fn not_a_constructor(ctor: &Value) -> String {
5504    let name = with_host(|h| match h.callable_name(ctor) {
5505        n if n.is_empty() => h.str_of(ctor),
5506        n => n,
5507    });
5508    type_error(&format!("{name} is not a constructor"))
5509}
5510
5511/// Whether a constructor's return value is an object (so `new` yields it instead
5512/// of the fresh instance). In JS "object" includes functions — the `router`
5513/// package's constructor `return router` (a function) must be honored, or the
5514/// returned router loses its callable identity.
5515fn returns_object(r: &Value) -> bool {
5516    matches!(
5517        with_host(|h| h.get(r).cloned()),
5518        Some(JsObj::Object(_))
5519            | Some(JsObj::Array(_))
5520            | Some(JsObj::Map { .. })
5521            | Some(JsObj::Set { .. })
5522            | Some(JsObj::Func(_))
5523            | Some(JsObj::Class(_))
5524            | Some(JsObj::BoundFunc { .. })
5525            | Some(JsObj::BoundMethod { .. })
5526            | Some(JsObj::RegExp(_))
5527    )
5528}
5529
5530/// Construct a `class` instance: allocate the object linked to `C.prototype`,
5531/// run field initializers + the constructor (which may call `super(...)`).
5532fn construct_class(
5533    class_val: &Value,
5534    args: Vec<Value>,
5535    new_target: Value,
5536) -> Result<Value, String> {
5537    let cv = match with_host(|h| h.get(class_val).cloned()) {
5538        Some(JsObj::Class(c)) => c,
5539        _ => return Err(type_error("not a class")),
5540    };
5541    // Resolve the prototype of the *most-derived* class being `new`ed, so an
5542    // instance created through a `super()` chain still delegates to the leaf
5543    // prototype (correct method resolution).
5544    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
5545        Some(JsObj::Class(c)) => c.proto.clone(),
5546        _ => cv.proto.clone(),
5547    };
5548    let inst = with_host(|h| {
5549        let o = h.new_object(IndexMap::new());
5550        h.set_proto(&o, leaf_proto.clone());
5551        o
5552    });
5553    // A constructor that returns an object replaces the instance (`new` semantics).
5554    match run_class_ctor(&cv, &inst, args, &new_target)? {
5555        Some(obj) if returns_object(&obj) => Ok(obj),
5556        _ => Ok(inst),
5557    }
5558}
5559
5560/// Run one class's field initializers then its constructor on an existing
5561/// instance. Returns the constructor's explicit object return (if any). For a
5562/// base class this is the whole init; for a derived class the constructor body
5563/// reaches `super(...)` which recurses into the parent.
5564fn run_class_ctor(
5565    cv: &ClassVal,
5566    inst: &Value,
5567    args: Vec<Value>,
5568    new_target: &Value,
5569) -> Result<Option<Value>, String> {
5570    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
5571    // that. A base class initializes fields before the constructor body.
5572    if cv.parent.is_none() {
5573        init_fields(cv, inst)?;
5574    }
5575    match &cv.ctor {
5576        Some(ctor_fn) => {
5577            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
5578                Some(JsObj::Func(f)) => f,
5579                _ => return Err(type_error("class constructor is not a function")),
5580            };
5581            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
5582            return Ok(Some(r));
5583        }
5584        None => {
5585            // Default constructor: `constructor(...a){ super(...a); }` for a
5586            // derived class, empty for a base class.
5587            if let Some(parent) = &cv.parent {
5588                super_construct(parent, args, inst, new_target)?;
5589                init_fields(cv, inst)?;
5590            }
5591        }
5592    }
5593    Ok(None)
5594}
5595
5596/// Evaluate and assign a class's instance-field initializers on `inst`.
5597fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
5598    for (name, thunk, name_anon) in &cv.fields {
5599        init_one_field(inst, name, thunk, *name_anon)?;
5600    }
5601    Ok(())
5602}
5603
5604/// Evaluate ONE instance-field initializer thunk and install the result on
5605/// `inst`.
5606///
5607/// Shared by the base-class path (`init_fields`) and the derived-class path
5608/// that runs after `super(...)`; the two used to be separate loops, and only the
5609/// first canonicalized an array-index key.
5610///
5611/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
5612/// gives the function the name `f`. It is decided by the compiler from the
5613/// syntax, never from the value.
5614pub fn init_one_field(
5615    inst: &Value,
5616    name: &str,
5617    thunk: &Value,
5618    name_anon: bool,
5619) -> Result<(), String> {
5620    // The thunk is an arrow capturing the class scope; run it with `this`=inst
5621    // so `this.other`-referencing initializers work.
5622    let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
5623    with_host(|h| {
5624        if name_anon {
5625            let s = h.new_str(name.to_string());
5626            h.set_fn_prop(&val, "name", s);
5627        }
5628        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
5629            let is_new = !props.contains_key(name);
5630            props.insert(name.to_string(), val);
5631            if is_new && array_index(name).is_some() {
5632                canonicalize_own_keys(props);
5633            }
5634        }
5635    });
5636    Ok(())
5637}
5638
5639/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
5640/// kind (class vs plain function vs builtin) using the existing instance.
5641pub fn super_construct(
5642    parent: &Value,
5643    args: Vec<Value>,
5644    inst: &Value,
5645    new_target: &Value,
5646) -> Result<(), String> {
5647    match with_host(|h| h.get(parent).cloned()) {
5648        Some(JsObj::Class(pcv)) => run_class_ctor(&pcv, inst, args, new_target).map(|_| ()),
5649        Some(JsObj::Func(fv)) => {
5650            run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
5651            Ok(())
5652        }
5653        Some(JsObj::Builtin(name)) => {
5654            // Extending a builtin (e.g. `class E extends Error`): copy the built
5655            // object's own props onto the instance so the subclass instance
5656            // carries them.
5657            let built = crate::builtins::construct_builtin(&name, args)?;
5658            adopt_own_props(inst, &built);
5659            Ok(())
5660        }
5661        // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
5662        // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
5663        // to the target). node-js initializes an ALREADY-allocated `inst` rather
5664        // than adopting the constructor's return value, so what the proxy built
5665        // is moved across — the same move the builtin arm makes.
5666        Some(JsObj::Proxy { .. }) => {
5667            let built = construct_nt(parent, args, new_target.clone())?;
5668            adopt_own_props(inst, &built);
5669            Ok(())
5670        }
5671        _ => Err(type_error("super is not a constructor")),
5672    }
5673}
5674
5675/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
5676/// a parent constructor produces a fresh object but node-js's class model has
5677/// already allocated the instance `this` is bound to.
5678fn adopt_own_props(inst: &Value, built: &Value) {
5679    let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
5680        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
5681        _ => Vec::new(),
5682    });
5683    with_host(|h| {
5684        let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
5685        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
5686            for (k, v) in entries {
5687                props.insert(k, v);
5688            }
5689            canonicalize_own_keys(props);
5690        }
5691        // The copied slots keep the attributes the source gave them, so
5692        // `class E extends Error` instances hide `message`/`stack` too.
5693        for k in keys {
5694            let a = h.prop_attrs(built, &k);
5695            h.set_prop_attrs(inst, &k, a);
5696        }
5697    });
5698}
5699
5700// ── class construction (runtime) ─────────────────────────────────────────────
5701
5702/// Build a class constructor value from its parts. The compiler emits (via
5703/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
5704/// undefined for a default constructor); methods/getters/setters/statics/fields
5705/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
5706pub fn build_class(name: &str, parent: Value, ctor: Value) -> Value {
5707    // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
5708    // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
5709    // trap and so re-enters the host, which the borrow below cannot allow.
5710    // Without it the link fell back to `Object.prototype` and every inherited
5711    // method went missing.
5712    let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
5713        .then(|| crate::builtins::get_property(&parent, "prototype").ok())
5714        .flatten();
5715    with_host(|h| {
5716        let parent_opt = if matches!(parent, Value::Undef) {
5717            None
5718        } else {
5719            Some(parent.clone())
5720        };
5721        // The class prototype delegates to the parent's prototype (or
5722        // Object.prototype for a base class). Extending a builtin error links to
5723        // that error's prototype so `instanceof Error` holds for the subclass.
5724        let parent_proto = match &parent_opt {
5725            Some(_) if proxy_parent_proto.is_some() => {
5726                proxy_parent_proto.clone().expect("checked is_some")
5727            }
5728            Some(p) => match h.get(p).cloned() {
5729                Some(JsObj::Class(pc)) => pc.proto.clone(),
5730                Some(JsObj::Builtin(bn)) => {
5731                    h.ensure_error_protos();
5732                    error_proto_of(h, &bn)
5733                        .or_else(|| h.fn_prop(p, "prototype"))
5734                        .unwrap_or_else(|| h.object_proto())
5735                }
5736                _ => h
5737                    .fn_prop(p, "prototype")
5738                    .unwrap_or_else(|| h.object_proto()),
5739            },
5740            None => h.object_proto(),
5741        };
5742        let proto = h.new_object(IndexMap::new());
5743        h.set_proto(&proto, parent_proto);
5744        let ctor_opt = if matches!(ctor, Value::Undef) {
5745            None
5746        } else {
5747            Some(ctor.clone())
5748        };
5749        // Give the constructor closure its home class (for `super.method()`), and
5750        // record its `.name`.
5751        if let Some(cf) = &ctor_opt {
5752            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
5753                f.home_class = Some(name.to_string());
5754            }
5755        }
5756        let cval = ClassVal {
5757            name: name.to_string(),
5758            ctor: ctor_opt,
5759            parent: parent_opt,
5760            proto: proto.clone(),
5761            statics: IndexMap::new(),
5762            fields: Vec::new(),
5763        };
5764        let class_val = h.alloc(JsObj::Class(cval));
5765        h.class_registry.insert(name.to_string(), class_val.clone());
5766        // Link prototype → class (for instance display + `constructor`), and give
5767        // the class its own `prototype` fn-prop so `C.prototype` reads work.
5768        h.tag_proto_class(&proto, class_val.clone());
5769        h.set_fn_prop(&class_val, "prototype", proto.clone());
5770        // `Class.prototype.constructor === Class`.
5771        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
5772            p.insert("constructor".to_string(), class_val.clone());
5773        }
5774        h.hide_prop(&proto, "constructor");
5775        class_val
5776    })
5777}
5778
5779/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
5780/// `member::*` tag; `is_static` targets the constructor side.
5781pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
5782    with_host(|h| {
5783        let cname = match h.get(class_val) {
5784            Some(JsObj::Class(c)) => c.name.clone(),
5785            _ => String::new(),
5786        };
5787        // A private method/accessor: remember which class declared it, so a
5788        // brand-check failure can name the class the way node does. A static
5789        // FIELD is data, not a method, so it keeps the field wording.
5790        if name.starts_with('#') && kind != member::STATIC_FIELD {
5791            h.note_private_method(name);
5792        }
5793        // Give the method its home class for `super.x()`.
5794        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
5795            f.home_class = Some(cname);
5796        }
5797        // Static members live on the constructor (fn-props / static accessors);
5798        // instance members on the prototype.
5799        let target = if is_static {
5800            class_val.clone()
5801        } else {
5802            match h.get(class_val) {
5803                Some(JsObj::Class(c)) => c.proto.clone(),
5804                _ => return,
5805            }
5806        };
5807        match kind {
5808            member::GET => h.set_accessor(&target, name, Some(func), None),
5809            member::SET => h.set_accessor(&target, name, None, Some(func)),
5810            _ => {
5811                // A static field is enumerable (`Object.keys(C)` lists it) unlike
5812                // a method, so it must not reach the `hide_prop` below.
5813                if kind == member::STATIC_FIELD {
5814                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
5815                        c.statics.insert(name.to_string(), func.clone());
5816                    }
5817                    h.set_fn_prop(class_val, name, func);
5818                    return;
5819                }
5820                if is_static {
5821                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
5822                        c.statics.insert(name.to_string(), func.clone());
5823                    }
5824                    h.set_fn_prop(class_val, name, func);
5825                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
5826                    p.insert(name.to_string(), func);
5827                }
5828            }
5829        }
5830        // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
5831        // Evaluation), so `for (k in instance)` walking the prototype chain never
5832        // yields them and `Object.keys(C.prototype)` is empty.
5833        h.hide_prop(&target, name);
5834    });
5835}
5836
5837/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
5838pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
5839    with_host(|h| {
5840        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
5841            c.fields.push((name.to_string(), thunk, name_anon));
5842        }
5843    });
5844}
5845
5846/// The `[[Prototype]]` object a constructor value hands to its instances
5847/// (`Ctor.prototype`), for `instanceof`.
5848fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
5849    match h.get(ctor) {
5850        Some(JsObj::Class(c)) => Some(c.proto.clone()),
5851        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
5852        // A builtin's prototype lives in one of two registries: the error
5853        // prototypes, or the native exotic prototypes (`Buffer.prototype`,
5854        // `Uint8Array.prototype`). Consulting only the first made `instanceof`
5855        // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
5856        // `Buffer.prototype instanceof Uint8Array` read false even though the
5857        // link was there — the instance case only passed via a native-tag
5858        // special case, which a prototype object does not carry.
5859        Some(JsObj::Builtin(name)) => h
5860            .error_protos
5861            .get(name)
5862            .or_else(|| h.native_protos.get(name))
5863            .cloned(),
5864        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
5865        _ => None,
5866    }
5867}
5868
5869/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
5870/// so a chain walk driven by that function can compare the two with `strict_eq`.
5871///
5872/// `ctor_prototype` answers only for the constructors whose prototype object
5873/// really exists on the heap (classes, user functions, the error and native
5874/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
5875/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
5876/// mirrors that fallback rather than reporting "no prototype" and failing every
5877/// comparison.
5878fn walk_target_prototype(ctor: &Value) -> Option<Value> {
5879    if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
5880        return Some(p);
5881    }
5882    let name = with_host(|h| match h.get(ctor) {
5883        Some(JsObj::Builtin(n)) => Some(n.clone()),
5884        _ => None,
5885    })?;
5886    if name == "Object" {
5887        return Some(with_host(|h| h.object_proto()));
5888    }
5889    Some(with_host(|h| {
5890        h.alloc(JsObj::Builtin(format!("{name}.prototype")))
5891    }))
5892}
5893
5894/// V8's "not a function" wording for a value that was expected to be callable.
5895/// A number/string/boolean is named WITH its value (`number 1 is not a
5896/// function`, `string "s" is not a function`); every other type is named by type
5897/// alone (`object is not a function`, `symbol is not a function`).
5898fn not_a_function_message(v: &Value) -> String {
5899    with_host(|h| match v {
5900        Value::Bool(b) => format!("boolean {b} is not a function"),
5901        Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
5902        Value::Str(s) => format!("string \"{s}\" is not a function"),
5903        Value::Obj(_) => match h.get(v) {
5904            Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
5905            Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
5906            Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
5907            _ => "object is not a function".into(),
5908        },
5909        _ => "object is not a function".into(),
5910    })
5911}
5912
5913/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
5914/// `ctor.prototype`.
5915pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
5916    // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
5917    // right-hand side REPLACES the prototype-chain walk entirely, and it is
5918    // consulted before the callability check — which is why a plain (uncallable)
5919    // object that defines it is a legal `instanceof` right-hand side.
5920    if matches!(ctor, Value::Obj(_)) {
5921        // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
5922        // plain function both land in the fn-prop side table (which
5923        // `class_static` reads, following the `extends` chain), NOT in an object
5924        // property map — so consulting only `lookup_chain` would find the object
5925        // literal form and silently miss the two forms V8 users actually write.
5926        let handler = with_host(|h| {
5927            h.class_static(ctor, "@@hasInstance")
5928                .or_else(|| lookup_chain(h, ctor, "@@hasInstance"))
5929        });
5930        // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
5931        // else that is not callable is a TypeError, so a data property here does
5932        // NOT fall back to the prototype walk.
5933        match handler {
5934            Some(f) if with_host(|h| is_callable(h, &f)) => {
5935                let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
5936                return Ok(with_host(|h| h.truthy(&r)));
5937            }
5938            Some(f)
5939                if !matches!(f, Value::Undef)
5940                    && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
5941            {
5942                return Err(type_error(&not_a_function_message(&f)));
5943            }
5944            _ => {}
5945        }
5946    }
5947    // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
5948    // `1 instanceof 3` throws even though the left side could never match.
5949    // Returning early on the left side skipped that check entirely.
5950    let ctor_callable = with_host(|h| {
5951        matches!(
5952            h.get(ctor),
5953            Some(JsObj::Func(_))
5954                | Some(JsObj::Class(_))
5955                | Some(JsObj::Builtin(_))
5956                | Some(JsObj::BoundFunc { .. })
5957        )
5958    });
5959    if !ctor_callable {
5960        // V8 has TWO messages here and they are not interchangeable: a primitive
5961        // right-hand side is "not an object", an object that is merely not
5962        // callable is "not callable". Only the second was implemented, so
5963        // `1 instanceof 3` reported nothing at all.
5964        return Err(type_error(if matches!(ctor, Value::Obj(_)) {
5965            "Right-hand side of 'instanceof' is not callable"
5966        } else {
5967            "Right-hand side of 'instanceof' is not an object"
5968        }));
5969    }
5970    // A non-object left-hand side is never an instance — but only after the
5971    // right-hand side has been validated above.
5972    if !matches!(obj, Value::Obj(_)) {
5973        return Ok(false);
5974    }
5975    // A Proxy shares no heap variant with its target, so the structural arms
5976    // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
5977    // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
5978    // walk here, which also gives a custom trap the final say.
5979    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
5980        with_host(|h| {
5981            h.ensure_error_protos();
5982            h.ensure_native_protos();
5983        });
5984        let Some(target) = walk_target_prototype(ctor) else {
5985            return Ok(false);
5986        };
5987        let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
5988        for _ in 0..100 {
5989            if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
5990                return Ok(false);
5991            }
5992            if with_host(|h| h.strict_eq(&cur, &target)) {
5993                return Ok(true);
5994            }
5995            cur = crate::builtins::prototype_of(&cur);
5996        }
5997        return Ok(false);
5998    }
5999    // Builtin constructors whose instances aren't prototype-linked in our model
6000    // (arrays/plain objects/functions) get a structural instanceof.
6001    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
6002        let kind = with_host(|h| h.get(obj).cloned());
6003        match name.as_str() {
6004            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
6005            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
6006            // Map/Set/Promise instances are distinct heap variants, not
6007            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
6008            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
6009            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
6010            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
6011            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
6012            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
6013            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
6014            // A RegExp is its own heap variant too, not a prototype-linked object.
6015            "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
6016            "Object" => {
6017                // Everything object-typed except a null-prototype object is an
6018                // Object instance.
6019                let is_obj = matches!(
6020                    kind,
6021                    Some(JsObj::Object(_))
6022                        | Some(JsObj::Array(_))
6023                        | Some(JsObj::Func(_))
6024                        | Some(JsObj::Class(_))
6025                        | Some(JsObj::Map { .. })
6026                        | Some(JsObj::Set { .. })
6027                        | Some(JsObj::Promise { .. })
6028                        | Some(JsObj::Generator { .. })
6029                        | Some(JsObj::RegExp(_))
6030                );
6031                if is_obj {
6032                    // A null-prototype object (Object.create(null) or
6033                    // setPrototypeOf(o, null)) is NOT an Object instance.
6034                    if with_host(|h| h.has_null_proto(obj)) {
6035                        return Ok(false);
6036                    }
6037                    return Ok(true);
6038                }
6039                return Ok(false);
6040            }
6041            // A Node `Buffer` IS a `Uint8Array` subclass instance.
6042            "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
6043                return Ok(true);
6044            }
6045            // Every typed array carries the same `TypedArray` tag; the constructor
6046            // it is an instance of is its ELEMENT KIND.
6047            k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
6048                return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
6049            }
6050            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
6051            // `TextEncoder`, …) is an instance of the builtin whose name matches
6052            // its hidden `@@native` tag.
6053            other => {
6054                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
6055                    return Ok(true);
6056                }
6057            }
6058        }
6059    }
6060    with_host(|h| h.ensure_error_protos());
6061    // The native exotic prototypes are built lazily; `instanceof` may be the
6062    // first thing to ask for them, so materialise them before the chain walk.
6063    with_host(|h| h.ensure_native_protos());
6064    let target = match with_host(|h| ctor_prototype(h, ctor)) {
6065        Some(p) => p,
6066        None => return Ok(false),
6067    };
6068    let mut cur = with_host(|h| h.proto_of(obj));
6069    while let Some(p) = cur {
6070        if with_host(|h| h.strict_eq(&p, &target)) {
6071            return Ok(true);
6072        }
6073        cur = with_host(|h| h.proto_of(&p));
6074    }
6075    Ok(false)
6076}
6077
6078// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
6079
6080impl JsHost {
6081    /// Swap the volatile execution context in one shot, returning the previous
6082    /// one — installs a generator's context on resume, pulls it back on suspend.
6083    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
6084        std::mem::swap(&mut self.frames, &mut c.frames);
6085        std::mem::swap(&mut self.error, &mut c.error);
6086        std::mem::swap(&mut self.exc, &mut c.exc);
6087        std::mem::swap(&mut self.signal, &mut c.signal);
6088        c
6089    }
6090    pub fn is_generator_val(&self, v: &Value) -> bool {
6091        matches!(self.get(v), Some(JsObj::Generator { .. }))
6092    }
6093    /// Whether `v` is an ASYNC generator object — the borrow-free form of
6094    /// [`is_async_generator`], usable from code already holding the host.
6095    pub fn is_async_gen_val(&self, v: &Value) -> bool {
6096        match self.get(v) {
6097            Some(JsObj::Generator { id }) => self
6098                .generators
6099                .get(*id as usize)
6100                .map(|g| g.async_gen)
6101                .unwrap_or(false),
6102            _ => false,
6103        }
6104    }
6105    pub fn gen_done(&self, id: u32) -> bool {
6106        self.generators
6107            .get(id as usize)
6108            .map(|g| g.done)
6109            .unwrap_or(true)
6110    }
6111    fn gen_started(&self, id: u32) -> bool {
6112        self.generators
6113            .get(id as usize)
6114            .map(|g| g.started)
6115            .unwrap_or(false)
6116    }
6117}
6118
6119/// Build a suspended generator whose body is `chunk`, run in a frame with the
6120/// already-bound `env`. Nothing executes until the first `gen_resume`.
6121fn make_generator(
6122    chunk: Chunk,
6123    env: Env,
6124    this_val: Option<Value>,
6125    home_class: Option<String>,
6126) -> Value {
6127    let home = home_class
6128        .as_ref()
6129        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6130    let frame = Frame {
6131        base_env: env.clone(),
6132        env,
6133        this_obj: this_val,
6134        new_target: None,
6135        home_class: home,
6136        line: 0,
6137        owner: None,
6138        is_module: false,
6139    };
6140    let id = with_host(|h| {
6141        let id = h.generators.len() as u32;
6142        h.generators.push(GenCell {
6143            coro: None,
6144            yielder: std::ptr::null(),
6145            ctx: GenContext {
6146                frames: vec![frame],
6147                ..GenContext::default()
6148            },
6149            done: false,
6150            started: false,
6151            inject: None,
6152            async_gen: false,
6153            queue: std::collections::VecDeque::new(),
6154            running: false,
6155            stack_floor: 0,
6156        });
6157        id
6158    });
6159    let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
6160        ensure_coroutine_floor();
6161        // Same thread → publish the yielder so `yield` (deep in the body's VM)
6162        // can reach it. Valid for the whole body lifetime.
6163        with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
6164        let r = run_chunk_on(chunk);
6165        // A `return` inside the body leaves a Return signal carrying the final
6166        // value; capture it so `.next()` reports it as the completion value.
6167        let ret = with_host(|h| match h.signal.take() {
6168            Some(Signal::Return(v)) => v,
6169            _ => Value::Undef,
6170        });
6171        r.map(|_| ret)
6172    };
6173    // The body's stack is allocated here rather than left to `Coroutine::new` so
6174    // that its size is ours to choose and, above all, so its `limit()` is known:
6175    // that address is what `stack_exhausted` must compare against while the body
6176    // runs, since a coroutine does NOT run on the thread stack pthread reports.
6177    // A refused reservation still yields a working generator on corosensei's own
6178    // 1 MiB default, with a floor derived on entry instead.
6179    let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
6180        Ok(stack) => {
6181            let floor = coro_stack_floor(&stack);
6182            (corosensei::Coroutine::with_stack(stack, body), floor)
6183        }
6184        Err(_) => (corosensei::Coroutine::new(body), 0),
6185    };
6186    with_host(|h| {
6187        h.generators[id as usize].coro = Some(coro);
6188        h.generators[id as usize].stack_floor = floor;
6189    });
6190    with_host(|h| h.alloc(JsObj::Generator { id }))
6191}
6192
6193/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
6194/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
6195pub fn gen_yield(v: Value) -> Result<Value, String> {
6196    let id = match CUR_GEN.with(|c| c.get()) {
6197        Some(id) => id,
6198        None => return Err(type_error("yield outside a generator")),
6199    };
6200    let yp = with_host(|h| h.generators[id as usize].yielder);
6201    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
6202    // only reach here from inside that body (its stack is live).
6203    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
6204    let sent = yielder.suspend(v);
6205    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
6206    // convert it into a Return signal / thrown value so the body unwinds and any
6207    // `finally` runs, exactly as a source-level `return`/`throw` would.
6208    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
6209        match inj {
6210            GenInject::Return(rv) => {
6211                with_host(|h| h.signal = Some(Signal::Return(rv)));
6212                return Ok(Value::Undef);
6213            }
6214            GenInject::Throw(ev) => {
6215                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
6216                with_host(|h| h.exc = Some(ev));
6217                return Err(msg);
6218            }
6219        }
6220    }
6221    Ok(sent)
6222}
6223
6224/// `generator.return(v)`: force the generator to complete, running any pending
6225/// `finally`. If it is already done (or never started) it just reports
6226/// `{value:v, done:true}` without executing the body.
6227pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
6228    let id = match with_host(|h| h.get(gen).cloned()) {
6229        Some(JsObj::Generator { id }) => id,
6230        _ => return Err(type_error("not a generator")),
6231    };
6232    // Not started yet (coro present, ctx never resumed) OR already done → no body
6233    // to unwind: complete immediately with the supplied value.
6234    let started = with_host(|h| h.gen_started(id));
6235    if with_host(|h| h.generators[id as usize].done) || !started {
6236        with_host(|h| h.generators[id as usize].done = true);
6237        return Ok(GenStep::Done(v));
6238    }
6239    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
6240    gen_resume(gen, Value::Undef)
6241}
6242
6243/// `generator.throw(e)`: inject a throw at the suspension point, running any
6244/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
6245pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
6246    let id = match with_host(|h| h.get(gen).cloned()) {
6247        Some(JsObj::Generator { id }) => id,
6248        _ => return Err(type_error("not a generator")),
6249    };
6250    let started = with_host(|h| h.gen_started(id));
6251    if with_host(|h| h.generators[id as usize].done) || !started {
6252        // A throw into a done/unstarted generator propagates to the caller.
6253        with_host(|h| h.generators[id as usize].done = true);
6254        let msg = with_host(|h| crate::builtins::error_string(h, &e));
6255        with_host(|h| h.exc = Some(e));
6256        return Err(msg);
6257    }
6258    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
6259    gen_resume(gen, Value::Undef)
6260}
6261
6262/// Outcome of resuming a generator: a yielded value (not done), or the final
6263/// completion value (done).
6264pub enum GenStep {
6265    Yield(Value),
6266    Done(Value),
6267}
6268
6269/// Resume a generator until its next `yield` or its body returns. Preserves the
6270/// shared host: the coroutine is taken out so the body re-enters `with_host`
6271/// freely, and the volatile context is swapped so the caller's frames/signal
6272/// survive the switch.
6273pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
6274    let id = match with_host(|h| h.get(gen).cloned()) {
6275        Some(JsObj::Generator { id }) => id,
6276        _ => return Err(type_error("not a generator")),
6277    };
6278    if with_host(|h| h.generators[id as usize].done) {
6279        return Ok(GenStep::Done(Value::Undef));
6280    }
6281    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
6282        Some(c) => c,
6283        None => return Err("TypeError: generator already executing".into()),
6284    };
6285    with_host(|h| h.generators[id as usize].started = true);
6286    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
6287    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
6288    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
6289    // The body runs on the coroutine's OWN stack, so the guard's floor has to
6290    // move with it and move back on suspend — generators nest, and a resume from
6291    // inside another generator must restore that one's floor, not the thread's.
6292    let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
6293    let caller_floor = swap_stack_floor(coro_floor);
6294
6295    let out = coro.resume(send); // no host borrow held; body drives its own VM
6296
6297    let measured = swap_stack_floor(caller_floor);
6298    // A coroutine on corosensei's default stack has no known bounds, so the
6299    // floor it measured for itself on first entry is kept for later resumes.
6300    if coro_floor == 0 && measured != 0 {
6301        with_host(|h| h.generators[id as usize].stack_floor = measured);
6302    }
6303    CUR_GEN.with(|c| c.set(prev));
6304    let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
6305    // A `throw` inside the body left the thrown VALUE in the generator's context,
6306    // which the swap above just stashed away. Hand it to the caller so the
6307    // rejection/catch keeps the original error object instead of a string rebuild.
6308    let thrown = gen_ctx.exc.take();
6309    with_host(|h| {
6310        if let Some(v) = thrown {
6311            h.exc = Some(v);
6312        }
6313        h.generators[id as usize].ctx = gen_ctx;
6314        h.generators[id as usize].coro = Some(coro);
6315    });
6316
6317    match out {
6318        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
6319        corosensei::CoroutineResult::Return(r) => {
6320            // Release the coroutine — and with it the mmap'd stack it owns —
6321            // the moment the body completes. `h.generators` only ever grows (an
6322            // id is never reused), so a program that awaits in a loop otherwise
6323            // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
6324            // the life of the process. A finished generator is never resumed:
6325            // `gen_resume` returns `Done` on the `done` flag before it looks.
6326            with_host(|h| {
6327                let g = &mut h.generators[id as usize];
6328                g.done = true;
6329                g.coro = None;
6330            });
6331            match r {
6332                Ok(v) => Ok(GenStep::Done(v)),
6333                Err(e) => Err(e),
6334            }
6335        }
6336    }
6337}
6338
6339/// Force a generator to completion (used by `.return()` and abandoned loops):
6340/// marks it done without running further.
6341pub fn gen_close(gen: &Value) {
6342    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
6343        with_host(|h| h.generators[id as usize].done = true);
6344    }
6345}
6346
6347// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
6348
6349/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
6350pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
6351    match v {
6352        Value::Undef => MapKey::Undef,
6353        Value::Bool(b) => MapKey::Bool(*b),
6354        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
6355        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
6356        Value::Str(s) => MapKey::Str((**s).clone()),
6357        Value::Obj(i) => match h.get(v) {
6358            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
6359            Some(JsObj::Null) => MapKey::Null,
6360            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
6361            _ => MapKey::Ref(*i),
6362        },
6363        _ => MapKey::Undef,
6364    }
6365}
6366
6367/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
6368fn norm_num_bits(f: f64) -> u64 {
6369    if f.is_nan() {
6370        return f64::NAN.to_bits();
6371    }
6372    if f == 0.0 {
6373        return 0.0f64.to_bits(); // fold -0 into +0
6374    }
6375    f.to_bits()
6376}
6377
6378/// Fully materialize any iterable into a vector of values.
6379pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
6380    // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
6381    // no heap variant `iter_vec` could recognise.
6382    if let Some(items) = crate::proxy::iterate(v)? {
6383        return Ok(items);
6384    }
6385    // Generators / user iterators must resume without a live host borrow.
6386    if with_host(|h| h.is_generator_val(v)) {
6387        let mut out = Vec::new();
6388        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
6389            out.push(x);
6390        }
6391        return Ok(out);
6392    }
6393    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
6394    if let Some(iter_fn) = user_iterator_fn(v) {
6395        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
6396        return drain_iterator(&iterator);
6397    }
6398    with_host(|h| h.iter_vec(v))
6399}
6400
6401// ── async iteration (`for await (… of …)`) ───────────────────────────────────
6402
6403/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
6404/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
6405/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
6406/// are awaited one at a time by `async_step`.
6407pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
6408    if let Some(f) = user_async_iterator_fn(src) {
6409        return invoke(&f, Vec::new(), Some(src.clone()));
6410    }
6411    // An `async function*` object IS its own async iterator; draining it into a
6412    // list here would run the whole body (and any `finally`) before the consumer
6413    // sees the first value.
6414    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
6415        if with_host(|h| h.generators[id as usize].async_gen) {
6416            return Ok(src.clone());
6417        }
6418    }
6419    let items = iter_all(src)?;
6420    Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
6421}
6422
6423/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
6424fn user_async_iterator_fn(v: &Value) -> Option<Value> {
6425    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
6426    if !is_plain {
6427        return None;
6428    }
6429    let f = with_host(|h| lookup_chain(h, v, "@@asyncIterator"));
6430    match f {
6431        Some(f) if with_host(|h| is_callable(h, &f)) => Some(f),
6432        _ => None,
6433    }
6434}
6435
6436/// One step of a `for await` loop: return a Promise that settles to a
6437/// `{value, done}` record. For a native async iterator this is `iter.next()`
6438/// (already a promise of the record). For the sync fallback it pops the next raw
6439/// value, awaits it, and packages `{value: resolved, done:false}` (or
6440/// `{done:true}` at exhaustion).
6441pub fn async_step(iterator: &Value) -> Result<Value, String> {
6442    // An `async function*` object: resume it through the await-aware driver.
6443    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
6444        if with_host(|h| h.generators[id as usize].async_gen) {
6445            return Ok(async_gen_step(iterator, Value::Undef));
6446        }
6447    }
6448    // Sync-fallback iterator: drive it here, awaiting each yielded value.
6449    if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
6450        if idx >= items.len() {
6451            // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
6452            // promise even at exhaustion, so the `done: true` step costs the same
6453            // two microtask ticks a value step does.
6454            let step = with_host(|h| h.new_promise());
6455            let sid = with_host(|h| h.promise_id(&step).unwrap());
6456            with_host(|h| {
6457                h.queue_micro_native(Box::new(move || {
6458                    resolve_promise_val(sid, iter_record(Value::Undef, true));
6459                    Ok(())
6460                }))
6461            });
6462            return Ok(step);
6463        }
6464        let raw = items[idx].clone();
6465        with_host(|h| {
6466            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
6467                *idx += 1;
6468            }
6469        });
6470        // Await the raw value (adopts a promise's resolution), then wrap.
6471        let step = with_host(|h| h.new_promise());
6472        let sid = with_host(|h| h.promise_id(&step).unwrap());
6473        let raw_p = promise_of(&raw);
6474        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
6475        subscribe_native(
6476            raw_id,
6477            Box::new(move |state, val| {
6478                if state == PromiseState::Rejected {
6479                    reject_promise_val(sid, val);
6480                } else {
6481                    resolve_promise_val(sid, iter_record(val, false));
6482                }
6483                Ok(())
6484            }),
6485        );
6486        return Ok(step);
6487    }
6488    // Native async iterator: `iter.next()` returns the {value,done} promise.
6489    let r = call_method(iterator, "next", Vec::new())?;
6490    Ok(promise_of(&r))
6491}
6492
6493/// If `v` has an own/inherited `Symbol.iterator` method (internal key
6494/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
6495fn user_iterator_fn(v: &Value) -> Option<Value> {
6496    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
6497    if !is_plain {
6498        return None;
6499    }
6500    // Full property resolution, not a stored-property lookup: a NATIVE-tagged
6501    // object (`URLSearchParams`, `Headers`) dispatches its methods through the
6502    // stdlib method table rather than a property map, so `lookup_chain` reported
6503    // no `Symbol.iterator` for one even though reading it gave a function —
6504    // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
6505    let f = crate::builtins::get_property(v, "@@iterator").ok()?;
6506    with_host(|h| is_callable(h, &f)).then_some(f)
6507}
6508
6509/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
6510/// exhaustion.
6511pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
6512    let mut out = Vec::new();
6513    loop {
6514        let step = call_method(iterator, "next", Vec::new())?;
6515        let done = get_prop_chain(&step, "done")?;
6516        if with_host(|h| h.truthy(&done)) {
6517            break;
6518        }
6519        out.push(get_prop_chain(&step, "value")?);
6520    }
6521    Ok(out)
6522}
6523
6524/// Property read that walks the prototype chain (used by iteration helpers).
6525pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
6526    crate::builtins::get_property(recv, name)
6527}
6528
6529/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
6530/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
6531/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
6532/// native-tagged instances) is an object and must be converted.
6533pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
6534    match v {
6535        Value::Obj(_) => matches!(
6536            h.get(v),
6537            None | Some(JsObj::Null)
6538                | Some(JsObj::Str(_))
6539                | Some(JsObj::Symbol { .. })
6540                | Some(JsObj::BigInt(_))
6541        ),
6542        _ => true,
6543    }
6544}
6545
6546/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
6547/// or `"string"`.
6548///
6549/// An object carrying a `Symbol.toPrimitive` method (internal key
6550/// `@@toPrimitive`) has it called with the hint and must return a primitive.
6551/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
6552/// the order reversed for the string hint — and takes the FIRST call whose
6553/// result is a primitive. An object that yields no primitive (a null-prototype
6554/// object has neither method) throws V8's
6555/// `TypeError: Cannot convert object to primitive value`.
6556///
6557/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
6558/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
6559/// to read `str_of` directly and so never invoked a user `valueOf`.
6560pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
6561    if with_host(|h| is_primitive(h, v)) {
6562        return Ok(v.clone());
6563    }
6564    if let Some(f) = with_host(|h| lookup_chain(h, v, "@@toPrimitive")) {
6565        if with_host(|h| is_callable(h, &f)) {
6566            let hv = with_host(|h| h.new_str(hint.to_string()));
6567            let r = invoke(&f, vec![hv], Some(v.clone()))?;
6568            if with_host(|h| is_primitive(h, &r)) {
6569                return Ok(r);
6570            }
6571            return Err(type_error("Cannot convert object to primitive value"));
6572        }
6573    }
6574    // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
6575    // `"string"`, which is why `new Date() + 1` concatenates while
6576    // `new Date() - 1` is arithmetic.
6577    let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
6578        "string"
6579    } else {
6580        hint
6581    };
6582    let order = if hint == "string" {
6583        ["toString", "valueOf"]
6584    } else {
6585        ["valueOf", "toString"]
6586    };
6587    for m in order {
6588        let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
6589        if !with_host(|h| is_callable(h, &f)) {
6590            continue;
6591        }
6592        // On a Proxy the resolved method is a thunk bound to the TARGET, so
6593        // invoking it directly would stringify the target — `String(new
6594        // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
6595        // native-code form. `call_method` re-dispatches the generic
6596        // `Function.prototype`/`Object.prototype` methods against the proxy.
6597        let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6598            call_method(v, m, Vec::new())?
6599        } else {
6600            invoke(&f, Vec::new(), Some(v.clone()))?
6601        };
6602        if with_host(|h| is_primitive(h, &r)) {
6603            return Ok(r);
6604        }
6605    }
6606    // Every object except a null-prototype one inherits `Object.prototype
6607    // .toString`, which always returns a string — so the exhausted-methods
6608    // TypeError is reachable only there. The exotics whose property funnel has
6609    // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
6610    // get the same `[object Tag]` brand V8 gives them.
6611    if !with_host(|h| h.has_null_proto(v)) {
6612        return crate::builtins::proto_method(v, "Object:toString", Vec::new());
6613    }
6614    Err(type_error("Cannot convert object to primitive value"))
6615}
6616
6617/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
6618/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
6619/// is absent or returns an object — is invoked), then rendered by `str_of`.
6620/// Returns a heap string value.
6621pub fn to_string_value(v: &Value) -> Result<Value, String> {
6622    let p = to_primitive(v, "string")?;
6623    // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
6624    // refuses. `String(sym)` is the documented exception and is handled at that
6625    // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
6626    // `[sym].join()`) rejects the way node does instead of silently rendering
6627    // `Symbol(desc)`.
6628    if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
6629        return Err(type_error("Cannot convert a Symbol value to a string"));
6630    }
6631    Ok(with_host(|h| {
6632        let s = h.str_of(&p);
6633        h.new_str(s)
6634    }))
6635}
6636
6637/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
6638/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
6639pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
6640    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
6641        return Ok(with_host(|h| {
6642            let s = h.str_of(v);
6643            h.new_str(s)
6644        }));
6645    }
6646    to_string_value(v)
6647}
6648
6649/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
6650/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
6651/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
6652/// under the host borrow and so can never invoke a JS `valueOf`.
6653pub fn to_number_value(v: &Value) -> Result<f64, String> {
6654    // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
6655    // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
6656    // and `+Symbol()` are both `TypeError` on node v26.7.0.
6657    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
6658        return Err(type_error("Cannot convert a Symbol value to a number"));
6659    }
6660    if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
6661        return Ok(n);
6662    }
6663    let p = to_primitive(v, "number")?;
6664    Ok(with_host(|h| h.to_number(&p)))
6665}
6666
6667/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
6668/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
6669/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
6670pub fn to_property_key(v: &Value) -> Result<String, String> {
6671    // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
6672    // `o[sym]`); only an object key pays for the conversion.
6673    if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
6674        return Ok(k);
6675    }
6676    let p = to_primitive(v, "string")?;
6677    Ok(with_host(|h| h.str_of(&p)))
6678}
6679
6680/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
6681/// target is (10.5: the `[[Call]]` slot is installed only for a callable
6682/// target), so `typeof` and every `is_callable` guard agree on one answer.
6683pub fn is_callable(h: &JsHost, v: &Value) -> bool {
6684    match h.get(v) {
6685        Some(JsObj::Func(_))
6686        | Some(JsObj::Builtin(_))
6687        | Some(JsObj::BoundMethod { .. })
6688        | Some(JsObj::BoundFunc { .. })
6689        | Some(JsObj::Class(_)) => true,
6690        Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
6691        _ => false,
6692    }
6693}
6694
6695/// Walk `recv`'s own props then its prototype chain for `key`, returning the
6696/// stored value (methods, inherited data props). Does NOT invoke accessors.
6697pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
6698    if let Some(JsObj::Object(p)) = h.get(recv) {
6699        if let Some(v) = p.get(key) {
6700            return Some(v.clone());
6701        }
6702    }
6703    let mut cur = h.proto_of(recv);
6704    while let Some(p) = cur {
6705        // A chain link may be a plain object OR a function/class (the `router`
6706        // package sets `Router.prototype = function(){}` and hangs its methods off
6707        // that function, so the methods live in the fn-prop side table).
6708        match h.get(&p) {
6709            Some(JsObj::Object(props)) => {
6710                if let Some(v) = props.get(key) {
6711                    return Some(v.clone());
6712                }
6713            }
6714            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6715                if let Some(v) = h.fn_prop(&p, key) {
6716                    return Some(v);
6717                }
6718            }
6719            _ => {}
6720        }
6721        cur = h.proto_of(&p);
6722    }
6723    None
6724}
6725
6726/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
6727pub fn lookup_accessor(
6728    h: &JsHost,
6729    recv: &Value,
6730    key: &str,
6731) -> Option<(Option<Value>, Option<Value>)> {
6732    if let Some(a) = h.own_accessor(recv, key) {
6733        return Some(a);
6734    }
6735    let mut cur = h.proto_of(recv);
6736    while let Some(p) = cur {
6737        if let Some(a) = h.own_accessor(&p, key) {
6738            return Some(a);
6739        }
6740        cur = h.proto_of(&p);
6741    }
6742    None
6743}
6744
6745/// Register a builtin error prototype (for `instanceof Error` etc.).
6746pub fn set_error_proto(name: &str, proto: Value) {
6747    with_host(|h| {
6748        h.error_protos.insert(name.to_string(), proto);
6749    });
6750}
6751pub fn error_proto(name: &str) -> Option<Value> {
6752    with_host(|h| h.error_protos.get(name).cloned())
6753}
6754/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
6755pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
6756    h.error_protos.get(name).cloned()
6757}
6758
6759impl JsHost {
6760    /// `Error.prototype.toString` for an object whose prototype chain reaches
6761    /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
6762    /// `None` for anything that is not an error, so the caller keeps its own
6763    /// stringification.
6764    pub fn error_to_string(&self, v: &Value) -> Option<String> {
6765        let base = self.error_protos.get("Error")?;
6766        let mut cur = self.proto_of(v);
6767        let mut is_error = false;
6768        while let Some(p) = cur {
6769            if self.strict_eq(&p, base) {
6770                is_error = true;
6771                break;
6772            }
6773            cur = self.proto_of(&p);
6774        }
6775        if !is_error {
6776            return None;
6777        }
6778        let name = lookup_chain(self, v, "name")
6779            .map(|n| self.str_of(&n))
6780            .unwrap_or_else(|| "Error".into());
6781        let message = lookup_chain(self, v, "message")
6782            .map(|m| self.str_of(&m))
6783            .unwrap_or_default();
6784        // Node's internal coded errors override `toString` as
6785        // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
6786        // `@@nodeError` tag marks the errors `synth_error` built from a
6787        // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
6788        // property still stringifies plainly.
6789        if let Some(JsObj::Object(p)) = self.get(v) {
6790            if p.contains_key("@@nodeError") {
6791                if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
6792                    return Some(format!("{name} [{code}]: {message}"));
6793                }
6794            }
6795        }
6796        Some(match (name.is_empty(), message.is_empty()) {
6797            (true, _) => message,
6798            (false, true) => name,
6799            (false, false) => format!("{name}: {message}"),
6800        })
6801    }
6802}
6803
6804/// The set of builtin error constructor names forming the error hierarchy.
6805pub const ERROR_NAMES: &[&str] = &[
6806    "Error",
6807    "TypeError",
6808    "RangeError",
6809    "SyntaxError",
6810    "ReferenceError",
6811    "EvalError",
6812    "URIError",
6813    "AggregateError",
6814    // `assert`'s error class. It is NOT a global (node exposes it only as
6815    // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
6816    // has to be a name `synth_error` recognizes: without it the head
6817    // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
6818    // the `Error` branch with the WHOLE head kept as the message, so `e.name`
6819    // was `Error` and `e.message` carried a prefix node keeps out of it.
6820    "AssertionError",
6821];
6822
6823impl JsHost {
6824    /// Lazily build the builtin error prototype chain: `Error.prototype →
6825    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
6826    /// Populated once; instances link to these so `e instanceof TypeError` and
6827    /// `e instanceof Error` both hold.
6828    /// The real `Buffer.prototype` object, building the
6829    /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
6830    /// first use.
6831    ///
6832    /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
6833    /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
6834    /// `instanceof` had to be special-cased around it. Each prototype is a
6835    /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
6836    /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
6837    /// way it did when `Buffer.prototype` was a `Builtin` namespace.
6838    pub fn ensure_native_protos(&mut self) {
6839        if self.native_protos.contains_key("Buffer") {
6840            return;
6841        }
6842        let obj_proto = self.object_proto();
6843        // `Object.prototype` is the one builtin prototype that already existed as
6844        // a real object (it is the chain root). Register it so `Object.prototype`
6845        // reads resolve to THAT object rather than a fresh `Builtin` namespace —
6846        // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
6847        // compares a real object against a thunk and reads false.
6848        self.native_protos
6849            .insert("Object".to_string(), obj_proto.clone());
6850        for m in crate::builtins::OBJECT_PROTO_METHODS {
6851            let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
6852            if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
6853                p.insert((*m).to_string(), thunk);
6854            }
6855            self.hide_prop(&obj_proto, m);
6856        }
6857        // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
6858        // Object.prototype`, which is the chain node v26.7.0 really has. The
6859        // shared iteration methods (`every`, `map`, `filter`, …) live on the
6860        // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
6861        // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
6862        // Node while the intermediate owns it. `%TypedArray%` is not a global,
6863        // so it is reachable only by walking the chain — exactly as in Node.
6864        // Every element kind gets its own prototype hanging off the shared
6865        // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
6866        // `Int32Array.prototype` rather than some other kind's. Linking them all
6867        // to `Uint8Array.prototype` would have been the easy version and would
6868        // have made an `Int32Array` claim the wrong prototype.
6869        let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
6870        for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
6871            chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
6872        }
6873        // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
6874        chain.push(("Buffer", Value::Undef));
6875        let mut prev: Option<Value> = None;
6876        for (ctor, parent) in chain.drain(..) {
6877            let proto = self.new_object(IndexMap::new());
6878            // Each kind hangs off the shared intermediate; `Buffer` hangs off
6879            // `Uint8Array.prototype`; the intermediate itself off
6880            // `Object.prototype`.
6881            let parent = match ctor {
6882                "TypedArray" => parent,
6883                "Buffer" => self
6884                    .native_protos
6885                    .get("Uint8Array")
6886                    .cloned()
6887                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
6888                _ => self
6889                    .native_protos
6890                    .get("TypedArray")
6891                    .cloned()
6892                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
6893            };
6894            self.set_proto(&proto, parent);
6895            // `%TypedArray%.prototype` has no reachable constructor global, so
6896            // it gets no `constructor` slot (Node's is the anonymous
6897            // `%TypedArray%` intrinsic).
6898            if ctor != "TypedArray" {
6899                let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
6900                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
6901                    p.insert("constructor".into(), ctor_val);
6902                }
6903                self.hide_prop(&proto, "constructor");
6904            }
6905            let methods: &[&str] = match ctor {
6906                "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
6907                "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
6908                // A kind's prototype owns no methods; it inherits them from the
6909                // intermediate above. It does own `BYTES_PER_ELEMENT`, which is
6910                // per-kind and which Node really keeps there (measured:
6911                // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
6912                _ => &[],
6913            };
6914            if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
6915                let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
6916                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
6917                    p.insert("BYTES_PER_ELEMENT".into(), bpe);
6918                }
6919                self.hide_prop(&proto, "BYTES_PER_ELEMENT");
6920            }
6921            for m in methods {
6922                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
6923                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
6924                    p.insert((*m).to_string(), thunk);
6925                }
6926                self.hide_prop(&proto, m);
6927            }
6928            self.native_protos.insert(ctor.to_string(), proto.clone());
6929            prev = Some(proto);
6930        }
6931    }
6932
6933    /// The real prototype object for a builtin exotic, if it has one.
6934    pub fn native_proto(&self, ctor: &str) -> Option<Value> {
6935        self.native_protos.get(ctor).cloned()
6936    }
6937
6938    /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
6939    /// `Hash`, `URLSearchParams`, …), built on first read and cached.
6940    ///
6941    /// `Ctor.prototype` used to read `undefined` for every native class outside the
6942    /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
6943    /// that libraries still use. `iconv-lite`'s internal codec — reached from
6944    /// `raw-body` on every `express.json()` request — does exactly this:
6945    ///
6946    /// ```text
6947    /// var StringDecoder = require('string_decoder').StringDecoder;
6948    /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
6949    /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6950    /// InternalDecoder.prototype = StringDecoder.prototype;
6951    /// ```
6952    ///
6953    /// The first line threw `Cannot read properties of undefined (reading 'end')`.
6954    ///
6955    /// Methods come from `stdlib::instance_method_lists`, the same table a method
6956    /// READ consults, so the prototype can never advertise a name the dispatcher
6957    /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
6958    /// dispatches against its invoke-time `this`, so a subclass instance whose
6959    /// prototype IS this object gets the native implementation. Returns `None` for
6960    /// a tag with no instance methods, leaving those constructors as they were.
6961    pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
6962        if let Some(p) = self.native_protos.get(ctor) {
6963            return Some(p.clone());
6964        }
6965        let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
6966        if own.is_empty() && emitter.is_empty() {
6967            return None;
6968        }
6969        let obj_proto = self.object_proto();
6970        let proto = self.new_object(IndexMap::new());
6971        self.set_proto(&proto, obj_proto);
6972        let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
6973        if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
6974            p.insert("constructor".into(), ctor_val);
6975        }
6976        self.hide_prop(&proto, "constructor");
6977        for m in own.iter().chain(emitter.iter()) {
6978            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
6979            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
6980                p.insert((*m).to_string(), thunk);
6981            }
6982            self.hide_prop(&proto, m);
6983        }
6984        self.native_protos.insert(ctor.to_string(), proto.clone());
6985        Some(proto)
6986    }
6987
6988    pub fn ensure_error_protos(&mut self) {
6989        if !self.error_protos.is_empty() {
6990            return;
6991        }
6992        let obj_proto = self.object_proto();
6993        // Error.prototype first (the shared base).
6994        let err_proto = self.new_object(IndexMap::new());
6995        self.set_proto(&err_proto, obj_proto);
6996        let nm = self.new_str("Error");
6997        let empty = self.new_str("");
6998        let ctor = self.alloc(JsObj::Builtin("Error".into()));
6999        // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
7000        // not a fallback the stringifier applies when nothing else matches: it
7001        // exists precisely to shadow `Object.prototype.toString`. Without it,
7002        // the first read of `Error.prototype` or `Object.prototype` — which
7003        // `x instanceof Error` performs, so ordinary code triggers it —
7004        // materialised `Object.prototype.toString`, the chain lookup started
7005        // finding it, and `String(err)` flipped from `Error: m` to
7006        // `[object Error]` for the REST OF THE PROCESS, including errors
7007        // created before the read.
7008        let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
7009        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
7010            p.insert("name".into(), nm);
7011            p.insert("message".into(), empty);
7012            p.insert("constructor".into(), ctor);
7013            p.insert("toString".into(), to_string);
7014        }
7015        // Everything on `Error.prototype` is non-enumerable in V8.
7016        for k in ["name", "message", "constructor", "toString"] {
7017            self.hide_prop(&err_proto, k);
7018        }
7019        self.error_protos.insert("Error".into(), err_proto.clone());
7020        for name in &ERROR_NAMES[1..] {
7021            let p = self.new_object(IndexMap::new());
7022            self.set_proto(&p, err_proto.clone());
7023            let nm = self.new_str(*name);
7024            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
7025            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
7026                o.insert("name".into(), nm);
7027                o.insert("constructor".into(), ctor);
7028            }
7029            self.hide_prop(&p, "name");
7030            self.hide_prop(&p, "constructor");
7031            self.error_protos.insert((*name).to_string(), p);
7032        }
7033    }
7034}
7035
7036// ── Map/Set element access (used by builtins) ────────────────────────────────
7037
7038impl JsHost {
7039    /// A function's `.length`: the count of leading params before the first one
7040    /// with a default or the rest element.
7041    pub fn func_arity(&self, v: &Value) -> usize {
7042        let def_id = match self.get(v) {
7043            Some(JsObj::Func(f)) => Some(f.def_id),
7044            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
7045                Some(JsObj::Func(f)) => Some(f.def_id),
7046                _ => None,
7047            },
7048            _ => None,
7049        };
7050        match def_id.and_then(|id| self.funcs.get(id)) {
7051            Some(def) => def
7052                .params
7053                .iter()
7054                .take_while(|p| !p.rest && !p.has_default)
7055                .count(),
7056            None => 0,
7057        }
7058    }
7059
7060    pub fn is_map(&self, v: &Value) -> bool {
7061        matches!(self.get(v), Some(JsObj::Map { .. }))
7062    }
7063    pub fn is_set(&self, v: &Value) -> bool {
7064        matches!(self.get(v), Some(JsObj::Set { .. }))
7065    }
7066}
7067
7068// ── promises & the event loop ────────────────────────────────────────────────
7069
7070impl JsHost {
7071    /// Allocate a fresh pending promise, returning its heap value.
7072    pub fn new_promise(&mut self) -> Value {
7073        let id = self.promises.len() as u32;
7074        self.promises.push(PromiseCell {
7075            state: PromiseState::Pending,
7076            value: Value::Undef,
7077            reactions: Vec::new(),
7078            handled: false,
7079        });
7080        self.alloc(JsObj::Promise { id })
7081    }
7082    pub fn promise_id(&self, v: &Value) -> Option<u32> {
7083        match self.get(v) {
7084            Some(JsObj::Promise { id }) => Some(*id),
7085            _ => None,
7086        }
7087    }
7088    pub fn promise_state(&self, id: u32) -> PromiseState {
7089        self.promises[id as usize].state
7090    }
7091    pub fn promise_value(&self, id: u32) -> Value {
7092        self.promises[id as usize].value.clone()
7093    }
7094    pub fn promise_mark_handled(&mut self, id: u32) {
7095        self.promises[id as usize].handled = true;
7096    }
7097    /// Take the pending reactions of a promise (called on settle).
7098    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
7099        std::mem::take(&mut self.promises[id as usize].reactions)
7100    }
7101    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
7102        self.promises[id as usize].reactions.push(r);
7103    }
7104    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
7105        let c = &mut self.promises[id as usize];
7106        if c.state != PromiseState::Pending {
7107            return; // already settled — resolve/reject are one-shot
7108        }
7109        c.state = state;
7110        c.value = value;
7111    }
7112    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
7113        self.microtasks.push_back(Task::Js { cb, args });
7114    }
7115    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
7116        self.nextticks.push_back(Task::Js { cb, args });
7117    }
7118    /// Schedule a native (Rust) microtask — used by Promise reactions and async
7119    /// resumption.
7120    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
7121        self.microtasks.push_back(Task::Native(f));
7122    }
7123    /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
7124    /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
7125    /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
7126    /// `ref`/`unref` and `refresh` can find this entry again.
7127    pub fn add_timer(
7128        &mut self,
7129        delay: f64,
7130        callback: Value,
7131        args: Vec<Value>,
7132        interval: Option<f64>,
7133    ) -> u64 {
7134        let id = self.next_timer;
7135        self.next_timer += 1;
7136        // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
7137        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
7138        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
7139        self.macrotasks.push(Timer {
7140            id,
7141            delay,
7142            seq: id,
7143            callback,
7144            args,
7145            cancelled: false,
7146            interval,
7147            refed: true,
7148            deadline,
7149        });
7150        id
7151    }
7152    /// Re-arm a repeating timer that is about to fire, keeping its id (so a
7153    /// `clearInterval` from *inside* the callback cancels this very entry) and
7154    /// taking a fresh `seq` so same-delay peers still round-robin.
7155    ///
7156    /// Called BEFORE the callback runs: if it were called after, the entry would
7157    /// be absent while the callback executed and a `clearInterval(t)` there would
7158    /// cancel nothing, resurrecting an interval the program had stopped.
7159    fn rearm_timer(&mut self, t: &Timer, period: f64) {
7160        let seq = self.next_timer;
7161        self.next_timer += 1;
7162        let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
7163        self.macrotasks.push(Timer {
7164            id: t.id,
7165            delay: t.delay,
7166            seq,
7167            callback: t.callback.clone(),
7168            args: t.args.clone(),
7169            cancelled: false,
7170            interval: Some(period),
7171            refed: t.refed,
7172            deadline,
7173        });
7174    }
7175    /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
7176    /// timer. A no-op once the timer has fired or been cleared (Node likewise
7177    /// treats `ref`/`unref` on a dead timer as inert).
7178    pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
7179        for t in &mut self.macrotasks {
7180            if t.id == id && !t.cancelled {
7181                t.refed = refed;
7182            }
7183        }
7184    }
7185    /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
7186    /// A fired or cleared timer reports `false`, matching Node.
7187    pub fn timer_has_ref(&self, id: u64) -> bool {
7188        self.macrotasks
7189            .iter()
7190            .any(|t| t.id == id && !t.cancelled && t.refed)
7191    }
7192    /// `timeout.refresh()` — restart the countdown from now, as if the timer had
7193    /// just been scheduled.
7194    pub fn refresh_timer(&mut self, id: u64) {
7195        let now = Instant::now();
7196        for t in &mut self.macrotasks {
7197            if t.id == id && !t.cancelled {
7198                t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
7199            }
7200        }
7201    }
7202    /// Clone the I/O sender for a background I/O thread.
7203    pub fn io_sender(&self) -> Sender<IoTask> {
7204        self.io_tx.clone()
7205    }
7206    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
7207    /// alive.
7208    pub fn incr_handle(&mut self) {
7209        self.open_handles += 1;
7210    }
7211    /// Release a handle; the loop exits once this reaches `0` with empty queues.
7212    pub fn decr_handle(&mut self) {
7213        self.open_handles = self.open_handles.saturating_sub(1);
7214    }
7215    pub fn open_handles(&self) -> usize {
7216        self.open_handles
7217    }
7218    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
7219    /// path). Ties break by `seq`.
7220    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
7221        let idx = self
7222            .macrotasks
7223            .iter()
7224            .enumerate()
7225            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
7226            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
7227            .map(|(i, _)| i);
7228        idx.map(|i| self.macrotasks.remove(i))
7229    }
7230    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
7231    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
7232    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
7233        self.macrotasks
7234            .iter()
7235            .filter(|t| !t.cancelled)
7236            .map(|t| t.deadline)
7237            .min()
7238            .map(|d| d.saturating_duration_since(now))
7239    }
7240    pub fn cancel_timer(&mut self, id: u64) {
7241        for t in &mut self.macrotasks {
7242            if t.id == id {
7243                t.cancelled = true;
7244            }
7245        }
7246    }
7247    fn pop_next_timer(&mut self) -> Option<Timer> {
7248        // Earliest (delay, seq) fires first — a deterministic virtual clock.
7249        let idx = self
7250            .macrotasks
7251            .iter()
7252            .enumerate()
7253            .filter(|(_, t)| !t.cancelled)
7254            .min_by(|(_, a), (_, b)| {
7255                a.delay
7256                    .partial_cmp(&b.delay)
7257                    .unwrap_or(std::cmp::Ordering::Equal)
7258                    .then(a.seq.cmp(&b.seq))
7259            })
7260            .map(|(i, _)| i);
7261        idx.map(|i| self.macrotasks.remove(i))
7262    }
7263    fn next_microtask(&mut self) -> Option<Task> {
7264        // nextTick drains before promise microtasks (Node ordering).
7265        self.nextticks
7266            .pop_front()
7267            .or_else(|| self.microtasks.pop_front())
7268    }
7269    fn has_microtasks(&self) -> bool {
7270        !self.nextticks.is_empty() || !self.microtasks.is_empty()
7271    }
7272    /// Whether any pending timer is *referenced* — the timer half of Node's
7273    /// handle count. Only these keep the loop alive; unref'd timers still fire
7274    /// while something else holds the loop open, but never hold it themselves.
7275    fn has_refed_macrotasks(&self) -> bool {
7276        self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
7277    }
7278    /// Whether any pending timer repeats. A repeating timer cannot run on the
7279    /// virtual clock: virtual time never advances, so the interval would re-arm
7280    /// at the same instant forever, spinning a core and starving every
7281    /// longer-delay timer behind it. Its presence forces the real clock.
7282    fn has_pending_interval(&self) -> bool {
7283        self.macrotasks
7284            .iter()
7285            .any(|t| !t.cancelled && t.interval.is_some())
7286    }
7287}
7288
7289/// Drive the event loop to quiescence.
7290///
7291/// **Liveness** is Node's handle count: the loop runs while a microtask is
7292/// pending, an open handle is registered (a listening server, a live socket, an
7293/// in-flight async op), or a *referenced* timer is still pending. That last term
7294/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
7295/// does in Node — the interval re-arms itself, so a ref'd timer is always
7296/// pending and the loop never reaches its exit condition.
7297///
7298/// Two **clock regimes**, selected per iteration:
7299///
7300/// - **Virtual clock** (no open handles and no repeating timer): the original
7301///   deterministic path — fire the earliest `(delay, seq)` timer immediately, no
7302///   real waiting. Parity output and test speed for ordinary `setTimeout`
7303///   scripts are unchanged.
7304/// - **Real clock** (an open handle, or any pending interval): fire every timer
7305///   whose wall-clock deadline has passed, then BLOCK on the I/O channel
7306///   (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
7307///   timers) and run the received `IoTask` on the main thread. The host keeps
7308///   its own `Sender`, so `recv` never disconnects while the process should stay
7309///   alive.
7310///
7311///   A repeating timer *must* take this path: virtual time never advances, so an
7312///   interval on the virtual clock would re-fire at the same instant forever,
7313///   spinning a core and starving every longer-delay timer behind it.
7314///
7315/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
7316pub fn run_event_loop() -> Result<(), String> {
7317    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
7318    // host borrow); restore it afterward so a re-entrant run reuses the channel.
7319    let rx = with_host(|h| h.io_rx.take());
7320    let result = drive_event_loop(rx.as_ref());
7321    with_host(|h| h.io_rx = rx);
7322    result
7323}
7324
7325fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
7326    loop {
7327        // 1) Exhaust the microtask queue (nextTick before promise reactions),
7328        //    then report anything that rejected with nobody watching.
7329        while let Some(task) = with_host(|h| h.next_microtask()) {
7330            task.run()?;
7331        }
7332        check_unhandled_rejections()?;
7333
7334        // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
7335        //    process exits, dropping any unref'd timers still pending — which is
7336        //    why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
7337        //    timer behind a ref'd one does.
7338        let alive =
7339            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
7340        if !alive {
7341            break;
7342        }
7343
7344        // 3) Pick the clock regime for this turn.
7345        let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
7346        if virtual_clock {
7347            // ── virtual-clock regime (unchanged for one-shot timers) ─────────
7348            match with_host(|h| h.pop_next_timer()) {
7349                Some(t) => fire_timer(t)?,
7350                // Unreachable while `alive` holds (a ref'd timer must exist),
7351                // but exiting is the safe reading of "nothing left to run".
7352                None => break,
7353            }
7354            continue;
7355        }
7356
7357        // ── real-clock / blocking-I/O regime ─────────────────────────────────
7358        let now = Instant::now();
7359        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
7360            fire_timer(t)?;
7361            continue; // re-drain microtasks, re-check deadlines
7362        }
7363        // Nothing due and no pending microtasks: block for the next I/O event,
7364        // bounded by the soonest timer deadline so due timers still fire on time.
7365        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
7366        let timeout = with_host(|h| h.next_timer_timeout(now));
7367        let recv = match timeout {
7368            Some(d) => rx.recv_timeout(d),
7369            None => rx
7370                .recv()
7371                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
7372        };
7373        match recv {
7374            Ok(task) => task()?,
7375            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
7376            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
7377        }
7378    }
7379    Ok(())
7380}
7381
7382/// Run one due timer's callback, first re-arming it if it repeats.
7383///
7384/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
7385/// issued from inside that callback cancels the next occurrence. Re-arming
7386/// afterwards would leave the interval absent from the queue for the duration of
7387/// its own callback, so the `clear` would match nothing and the freshly pushed
7388/// entry would resurrect an interval the program had just stopped.
7389fn fire_timer(t: Timer) -> Result<(), String> {
7390    if let Some(period) = t.interval {
7391        with_host(|h| h.rearm_timer(&t, period));
7392    }
7393    invoke(&t.callback, t.args, None)?;
7394    Ok(())
7395}
7396
7397// ── async functions & promise resolution (native) ────────────────────────────
7398
7399/// Drive a freshly-built async coroutine and return its result promise.
7400fn run_async(gen: Value) -> Value {
7401    let result = with_host(|h| h.new_promise());
7402    let rid = with_host(|h| h.promise_id(&result).unwrap());
7403    drive_async(gen, rid, Value::Undef);
7404    result
7405}
7406
7407/// Resume an async coroutine one step, wiring `await` continuations to promise
7408/// settlement.
7409fn drive_async(gen: Value, rid: u32, send: Value) {
7410    match gen_resume(&gen, send) {
7411        Ok(GenStep::Yield(awaited)) => {
7412            let ap = promise_of(&awaited);
7413            let aid = with_host(|h| h.promise_id(&ap).unwrap());
7414            let gen2 = gen.clone();
7415            subscribe_native(
7416                aid,
7417                Box::new(move |state, val| {
7418                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
7419                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
7420                    let tag = if state == PromiseState::Rejected {
7421                        1.0
7422                    } else {
7423                        0.0
7424                    };
7425                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
7426                    drive_async(gen2, rid, packet);
7427                    Ok(())
7428                }),
7429            );
7430        }
7431        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
7432        Err(e) => {
7433            let ev = take_exc_or_error(&e);
7434            reject_promise_val(rid, ev);
7435        }
7436    }
7437}
7438
7439/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
7440/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
7441pub fn await_value(awaited: Value) -> Result<Value, String> {
7442    // Inside an `async function*`, `await` and `yield` share one coroutine
7443    // yielder, so an awaited value has to be tagged or the driver would hand it
7444    // to the consumer as if the body had yielded it.
7445    let awaited = match CUR_GEN.with(|c| c.get()) {
7446        Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
7447            let mut m = IndexMap::new();
7448            m.insert(AWAIT_MARKER.to_string(), awaited);
7449            h.new_object(m)
7450        }),
7451        _ => awaited,
7452    };
7453    let packet = gen_yield(awaited)?;
7454    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
7455    let tag = items
7456        .first()
7457        .map(|v| with_host(|h| h.to_number(v)))
7458        .unwrap_or(0.0);
7459    let val = items.get(1).cloned().unwrap_or(Value::Undef);
7460    if tag == 1.0 {
7461        with_host(|h| h.exc = Some(val.clone()));
7462        Err(with_host(|h| crate::builtins::error_string(h, &val)))
7463    } else {
7464        Ok(val)
7465    }
7466}
7467
7468/// Hidden key marking an `await` suspension inside an async generator.
7469const AWAIT_MARKER: &str = "@@await";
7470
7471/// The operand of an `await` suspension, or `None` for a real `yield`.
7472fn await_marker(v: &Value) -> Option<Value> {
7473    with_host(|h| match h.get(v) {
7474        Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
7475        _ => None,
7476    })
7477}
7478
7479/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
7480/// hand back the promise its `{value, done}` record (or rejection) will settle.
7481///
7482/// All three of `.next`, `.return` and `.throw` come through here, so a request
7483/// never resumes the body while an earlier one is still suspended on an
7484/// internal `await`.
7485pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
7486    let step = with_host(|h| h.new_promise());
7487    let sid = with_host(|h| h.promise_id(&step).unwrap());
7488    let id = match with_host(|h| match h.get(gen) {
7489        Some(JsObj::Generator { id }) => Some(*id),
7490        _ => None,
7491    }) {
7492        Some(id) => id,
7493        None => return step,
7494    };
7495    with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
7496    pump_async_gen(gen.clone(), id);
7497    step
7498}
7499
7500/// One `.next(v)` of an `async function*`.
7501pub fn async_gen_step(gen: &Value, send: Value) -> Value {
7502    async_gen_enqueue(gen, GenReq::Next(send))
7503}
7504
7505/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
7506/// already in flight (the body may only be resumed by one request at a time).
7507fn pump_async_gen(gen: Value, id: u32) {
7508    if with_host(|h| h.generators[id as usize].running) {
7509        return;
7510    }
7511    let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
7512        return;
7513    };
7514    with_host(|h| h.generators[id as usize].running = true);
7515    start_async_gen_req(gen, sid, req);
7516}
7517
7518/// Begin one queued request: resume the body with the completion it carries,
7519/// then hand the outcome to the shared continuation.
7520///
7521/// A RETURN completion always Awaits its value before the body sees it — via
7522/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
7523/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
7524/// when it is not yet started or already completed. So a `.return()` settles one
7525/// microtask after a `.next()` or `.throw()` issued in its place would, and the
7526/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
7527/// `.return()` overtake the reactions of the `.next()` it followed.
7528fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
7529    if matches!(req, GenReq::Return(_)) {
7530        with_host(|h| {
7531            h.queue_micro_native(Box::new(move || {
7532                resume_async_gen_req(gen, sid, req);
7533                Ok(())
7534            }))
7535        });
7536        return;
7537    }
7538    resume_async_gen_req(gen, sid, req);
7539}
7540
7541/// Deliver a queued completion to the body and settle its step promise.
7542fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
7543    let step = match req {
7544        GenReq::Next(v) => gen_resume(&gen, v),
7545        GenReq::Return(v) => gen_return(&gen, v),
7546        GenReq::Throw(e) => gen_throw(&gen, e),
7547    };
7548    settle_async_gen_step(gen, sid, step);
7549}
7550
7551/// One request has settled: release the body and start the next queued request.
7552fn finish_async_gen_step(gen: Value, id: u32) {
7553    with_host(|h| h.generators[id as usize].running = false);
7554    pump_async_gen(gen, id);
7555}
7556
7557/// Whether `v` is an `async function*` object (its `.next()` yields promises).
7558pub fn is_async_generator(v: &Value) -> bool {
7559    let id = match with_host(|h| match h.get(v) {
7560        Some(JsObj::Generator { id }) => Some(*id),
7561        _ => None,
7562    }) {
7563        Some(id) => id,
7564        None => return false,
7565    };
7566    with_host(|h| h.generators[id as usize].async_gen)
7567}
7568
7569/// A `{ value, done }` iterator-result object.
7570fn iter_record(value: Value, done: bool) -> Value {
7571    with_host(|h| {
7572        let mut m = IndexMap::new();
7573        m.insert("value".to_string(), value);
7574        m.insert("done".to_string(), Value::Bool(done));
7575        h.new_object(m)
7576    })
7577}
7578
7579/// Resume a request that was suspended on an internal `await` (always a normal
7580/// completion — the awaited promise's outcome rides in `packet`).
7581fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
7582    let step = gen_resume(&gen, packet);
7583    settle_async_gen_step(gen, sid, step);
7584}
7585
7586/// Turn one body resumption into a settled step promise: transparently re-drive
7587/// internal `await` suspensions, and settle on the first REAL yield or on the
7588/// body's completion. Shared by the initial resume of a queued request and by
7589/// every await-resumption of it.
7590fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
7591    let id = match with_host(|h| match h.get(&gen) {
7592        Some(JsObj::Generator { id }) => Some(*id),
7593        _ => None,
7594    }) {
7595        Some(id) => id,
7596        None => return,
7597    };
7598    match step {
7599        Ok(GenStep::Yield(v)) => match await_marker(&v) {
7600            Some(awaited) => {
7601                // An internal `await`: settle it, then resume the body. The
7602                // request stays in flight across the suspension.
7603                let ap = promise_of(&awaited);
7604                let aid = with_host(|h| h.promise_id(&ap).unwrap());
7605                subscribe_native(
7606                    aid,
7607                    Box::new(move |state, val| {
7608                        let tag = if state == PromiseState::Rejected {
7609                            1.0
7610                        } else {
7611                            0.0
7612                        };
7613                        let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
7614                        drive_async_gen(gen.clone(), sid, packet);
7615                        Ok(())
7616                    }),
7617                );
7618            }
7619            // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
7620            // AWAITED before the step promise settles, so `yield somePromise`
7621            // hands the consumer the RESOLVED value (and costs its microtask).
7622            None => {
7623                let yp = promise_of(&v);
7624                let yid = with_host(|h| h.promise_id(&yp).unwrap());
7625                subscribe_native(
7626                    yid,
7627                    Box::new(move |state, val| {
7628                        if state == PromiseState::Rejected {
7629                            reject_promise_val(sid, val);
7630                        } else {
7631                            resolve_promise_val(sid, iter_record(val, false));
7632                        }
7633                        finish_async_gen_step(gen.clone(), id);
7634                        Ok(())
7635                    }),
7636                );
7637            }
7638        },
7639        Ok(GenStep::Done(v)) => {
7640            resolve_promise_val(sid, iter_record(v, true));
7641            finish_async_gen_step(gen, id);
7642        }
7643        Err(e) => {
7644            let ev = take_exc_or_error(&e);
7645            reject_promise_val(sid, ev);
7646            finish_async_gen_step(gen, id);
7647        }
7648    }
7649}
7650
7651/// A promise for `v`: `v` itself if it is already a promise, else a promise
7652/// resolved with `v`.
7653pub fn promise_of(v: &Value) -> Value {
7654    if with_host(|h| h.promise_id(v)).is_some() {
7655        return v.clone();
7656    }
7657    let p = with_host(|h| h.new_promise());
7658    let id = with_host(|h| h.promise_id(&p).unwrap());
7659    resolve_promise_val(id, v.clone());
7660    p
7661}
7662
7663/// Register a native reaction on promise `id` (schedules immediately if already
7664/// settled).
7665pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
7666    // A native continuation (`await`, promise adoption, `for await`) observes a
7667    // rejection exactly as a `.catch` does, so it is not "unhandled".
7668    with_host(|h| h.promise_mark_handled(id));
7669    let state = with_host(|h| h.promise_state(id));
7670    if state == PromiseState::Pending {
7671        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
7672    } else {
7673        let val = with_host(|h| h.promise_value(id));
7674        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
7675    }
7676}
7677
7678/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
7679/// else fulfill with it.
7680pub fn resolve_promise_val(id: u32, value: Value) {
7681    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
7682        return;
7683    }
7684    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
7685        if vid == id {
7686            // Resolving a promise with itself → reject with a TypeError.
7687            let e = with_host(|h| {
7688                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
7689            });
7690            reject_promise_val(id, e);
7691            return;
7692        }
7693        // A native promise is still a thenable, so the spec routes it through
7694        // `NewPromiseResolveThenableJob` too — one microtask before the adoption
7695        // is even registered. (`await` does NOT pay this: V8's await optimization
7696        // subscribes to a native promise directly, which `await_value` mirrors.)
7697        with_host(|h| {
7698            h.queue_micro_native(Box::new(move || {
7699                subscribe_native(
7700                    vid,
7701                    Box::new(move |state, val| {
7702                        with_host(|h| h.settle_promise(id, state, val.clone()));
7703                        schedule_reactions(id);
7704                        Ok(())
7705                    }),
7706                );
7707                Ok(())
7708            }))
7709        });
7710        return;
7711    }
7712    // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
7713    // through a dedicated job — the promise adopts what `then` reports, it is
7714    // never fulfilled WITH the thenable itself.
7715    if let Some(then) = thenable_then(&value) {
7716        with_host(|h| {
7717            h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
7718        });
7719        return;
7720    }
7721    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
7722    schedule_reactions(id);
7723}
7724
7725/// `value.then` if `value` is an object with a callable `then` — the test that
7726/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
7727fn thenable_then(value: &Value) -> Option<Value> {
7728    if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
7729        return None;
7730    }
7731    let then = with_host(|h| lookup_chain(h, value, "then"))?;
7732    with_host(|h| is_callable(h, &then)).then_some(then)
7733}
7734
7735/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
7736/// reject continuations and let it settle us. A throw out of `then` rejects.
7737fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
7738    let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
7739    let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
7740    if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
7741        let ev = take_exc_or_error(&e);
7742        reject_promise_val(id, ev);
7743    }
7744    Ok(())
7745}
7746
7747pub fn reject_promise_val(id: u32, value: Value) {
7748    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
7749        return;
7750    }
7751    with_host(|h| {
7752        h.settle_promise(id, PromiseState::Rejected, value);
7753        h.pending_rejections.push(id);
7754    });
7755    schedule_reactions(id);
7756}
7757
7758/// Report every promise that settled rejected since the last checkpoint and
7759/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
7760/// rejection becomes an uncaught exception (stderr + exit 1) unless a
7761/// `process.on('unhandledRejection')` listener takes it.
7762fn check_unhandled_rejections() -> Result<(), String> {
7763    loop {
7764        let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
7765        if ids.is_empty() {
7766            return Ok(());
7767        }
7768        for id in ids {
7769            let unhandled = with_host(|h| {
7770                h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
7771            });
7772            if !unhandled {
7773                continue;
7774            }
7775            // Report each promise at most once, however many checkpoints pass.
7776            with_host(|h| h.promise_mark_handled(id));
7777            let val = with_host(|h| h.promise_value(id));
7778            let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
7779            if listeners.is_empty() {
7780                let msg = with_host(|h| crate::builtins::error_string(h, &val));
7781                with_host(|h| h.exc = Some(val));
7782                return Err(msg);
7783            }
7784            let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
7785            for f in listeners {
7786                invoke(&f, vec![val.clone(), promise.clone()], None)?;
7787            }
7788        }
7789    }
7790}
7791
7792/// Drain a settled promise's reactions into microtasks.
7793fn schedule_reactions(id: u32) {
7794    let reactions = with_host(|h| h.take_reactions(id));
7795    let state = with_host(|h| h.promise_state(id));
7796    let value = with_host(|h| h.promise_value(id));
7797    for r in reactions {
7798        let value = value.clone();
7799        match r {
7800            PromiseReaction::Native(f) => {
7801                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
7802            }
7803            PromiseReaction::Js {
7804                on_ful,
7805                on_rej,
7806                result,
7807            } => {
7808                with_host(|h| {
7809                    h.queue_micro_native(Box::new(move || {
7810                        run_js_reaction(state, value, on_ful, on_rej, result)
7811                    }))
7812                });
7813            }
7814        }
7815    }
7816}
7817
7818/// Run a `.then` reaction: call the appropriate handler and settle the result
7819/// promise with its outcome (or pass through if there is no handler).
7820fn run_js_reaction(
7821    state: PromiseState,
7822    value: Value,
7823    on_ful: Value,
7824    on_rej: Value,
7825    result: Value,
7826) -> Result<(), String> {
7827    let rid = match with_host(|h| h.promise_id(&result)) {
7828        Some(i) => i,
7829        None => return Ok(()),
7830    };
7831    let handler = if state == PromiseState::Rejected {
7832        on_rej
7833    } else {
7834        on_ful
7835    };
7836    if with_host(|h| is_callable(h, &handler)) {
7837        match invoke(&handler, vec![value], None) {
7838            Ok(r) => resolve_promise_val(rid, r),
7839            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
7840        }
7841    } else if state == PromiseState::Rejected {
7842        reject_promise_val(rid, value);
7843    } else {
7844        resolve_promise_val(rid, value);
7845    }
7846    Ok(())
7847}
7848
7849/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
7850/// synthesized `Error` from the internal message.
7851pub fn take_exc_or_error(e: &str) -> Value {
7852    with_host(|h| {
7853        h.error.take();
7854        h.exc
7855            .take()
7856            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
7857    })
7858}
7859
7860/// Register a user `.then` reaction (JS handlers + result promise).
7861pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
7862    let id = match with_host(|h| h.promise_id(p)) {
7863        Some(i) => i,
7864        None => return Value::Undef,
7865    };
7866    with_host(|h| h.promise_mark_handled(id));
7867    let result = with_host(|h| h.new_promise());
7868    let reaction = PromiseReaction::Js {
7869        on_ful,
7870        on_rej,
7871        result: result.clone(),
7872    };
7873    let state = with_host(|h| h.promise_state(id));
7874    if state == PromiseState::Pending {
7875        with_host(|h| h.add_reaction(id, reaction));
7876    } else {
7877        let value = with_host(|h| h.promise_value(id));
7878        if let PromiseReaction::Js {
7879            on_ful,
7880            on_rej,
7881            result,
7882        } = reaction
7883        {
7884            with_host(|h| {
7885                h.queue_micro_native(Box::new(move || {
7886                    run_js_reaction(state, value, on_ful, on_rej, result)
7887                }))
7888            });
7889        }
7890    }
7891    result
7892}