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