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